<commit_before>/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
 * Copyright 2014 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*jslint node: true */

'use strict';

var http = require('http');
var path = require('path');
var fs = require('fs');

var mimeTypes = {
  '.css': 'text/css',
  '.html': 'text/html',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.svg': 'image/svg+xml',
  '.pdf': 'application/pdf',
  '.xhtml': 'application/xhtml+xml',
  '.gif': 'image/gif',
  '.ico': 'image/x-icon',
  '.png': 'image/png',
  '.log': 'text/plain',
  '.bcmap': 'application/octet-stream',
  '.properties': 'text/plain'
};

var defaultMimeType = 'application/octet-stream';

function WebServer() {
  this.root = '.';
  this.host = 'localhost';
  this.port = 8000;
  this.server = null;
  this.verbose = false;
  this.disableRangeRequests = false;
  this.hooks = {
    'GET': [],
    'POST': []
  };
}
WebServer.prototype = {
  start: function (callback) {
    this.server = http.createServer(this._handler.bind(this));
    this.server.listen(this.port, this.host, callback);
    console.log(
      'Server running at http://' + this.host + ':' + this.port + '/');
  },
  stop: function (callback) {
    this.server.close(callback);
    this.server = null;
  },
  _handler: function (req, res) {
    var agent = req.headers['user-agent'];
    var url = req.url;
    var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url);
    var pathPart = decodeURI(urlParts[1]), queryPart = urlParts[3];
    var verbose = this.verbose;

    var methodHooks = this.hooks[req.method];
    if (!methodHooks) {
      res.writeHead(405);
      res.end('Unsupported request method', 'utf8');
      return;
    }
    var handled = methodHooks.some(function (hook) {
      return hook(req, res);
    });
    if (handled) {
      return;
    }

    if (pathPart === '/favicon.ico') {
      fs.realpath(path.join(this.root, 'test/resources/favicon.ico'),
                  checkFile);
      return;
    }

    var disableRangeRequests = this.disableRangeRequests;

    var filePath;
    fs.realpath(path.join(this.root, pathPart), checkFile);

    function checkFile(err, file) {
      if (err) {
        res.writeHead(404);
        res.end();
        if (verbose) {
          console.error(url + ': not found');
        }
        return;
      }
      filePath = file;
      fs.stat(filePath, statFile);
    }

    var fileSize;

    function statFile(err, stats) {
      if (err) {
        res.writeHead(500);
        res.end();
        return;
      }

      fileSize = stats.size;
      var isDir = stats.isDirectory();
      if (isDir && !/\/$/.test(pathPart)) {
        res.setHeader('Location', pathPart + '/' + urlParts[2]);
        res.writeHead(301);
        res.end('Redirected', 'utf8');
        return;
      }
      if (isDir) {
        serveDirectoryIndex(filePath);
        return;
      }

      var range = req.headers['range'];
      if (range && !disableRangeRequests) {
        var rangesMatches = /^bytes=(\d+)\-(\d+)?/.exec(range);
        if (!rangesMatches) {
          res.writeHead(501);
          res.end('Bad range', 'utf8');
          if (verbose) {
            console.error(url + ': bad range: "' + range + '"');
          }
          return;
        }
        var start = +rangesMatches[1];
        var end = +rangesMatches[2];
        if (verbose) {
          console.log(url + ': range ' + start + ' - ' + end);
        }
        serveRequestedFileRange(filePath,
                                start,
                                isNaN(end) ? fileSize : (end + 1));
        return;
      }
      if (verbose) {
        console.log(url);
      }
      serveRequestedFile(filePath);
    }

    function serveDirectoryIndex(dir) {
      res.setHeader('Content-Type', 'text/html');
      res.writeHead(200);

      var content = '';
      if (queryPart === 'frame') {
        res.end('<frameset cols=*,200><frame name=pdf>' +
          '<frame src=\"' + encodeURI(pathPart) +
          '?side\"></frameset>', 'utf8');
        return;
      }
      var all = queryPart === 'all';
      fs.readdir(dir, function (err, files) {
        if (err) {
          res.end();
          return;
        }
        res.write('
PDFs of ' + pathPart + '
\n');
        if (pathPart !== '/') {
          res.write('..
\n');
        }
        files.forEach(function (file) {
          var stat = fs.statSync(path.join(dir, file));
          var item = pathPart + file;
          if (stat.isDirectory()) {
            res.write('' +
              file + '
\n');
            return;
          }
          var ext = path.extname(file).toLowerCase();
          if (ext === '.pdf') {
            res.write('' +
              file + '
\n');
          } else if (all) {
            res.write('' +
              file + '
\n');
          }
        });
        if (files.length === 0) {
          res.write('
no files found
\n');
        }
        if (!all && queryPart !== 'side') {
          res.write('

(only PDF files are shown, ' +
            'show all)
\n');
        }
        res.end('');
      });
    }

    function serveRequestedFile(filePath) {
      var stream = fs.createReadStream(filePath, {flags: 'rs'});

      stream.on('error', function (error) {
        res.writeHead(500);
        res.end();
      });

      var ext = path.extname(filePath).toLowerCase();
      var contentType = mimeTypes[ext] || defaultMimeType;

      if (!disableRangeRequests) {
        res.setHeader('Accept-Ranges', 'bytes');
      }
      res.setHeader('Content-Type', contentType);
      res.setHeader('Content-Length', fileSize);
      res.writeHead(200);

      stream.pipe(res);
    }

    function serveRequestedFileRange(filePath, start, end) {
      var stream = fs.createReadStream(filePath, {
        flags: 'rs', start: start, end: end - 1});

      stream.on('error', function (error) {
        res.writeHead(500);
        res.end();
      });

      var ext = path.extname(filePath).toLowerCase();
      var contentType = mimeTypes[ext] || defaultMimeType;

      res.setHeader('Accept-Ranges', 'bytes');
      res.setHeader('Content-Type', contentType);
      res.setHeader('Content-Length', (end - start));
      res.setHeader('Content-Range',
        'bytes ' + start + '-' + (end - 1) + '/' + fileSize);
      res.writeHead(206);

      stream.pipe(res);
    }

  }
};

exports.WebServer = WebServer;
<commit_msg>Remove unused variable<commit_after>/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
 * Copyright 2014 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*jslint node: true */

'use strict';

var http = require('http');
var path = require('path');
var fs = require('fs');

var mimeTypes = {
  '.css': 'text/css',
  '.html': 'text/html',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.svg': 'image/svg+xml',
  '.pdf': 'application/pdf',
  '.xhtml': 'application/xhtml+xml',
  '.gif': 'image/gif',
  '.ico': 'image/x-icon',
  '.png': 'image/png',
  '.log': 'text/plain',
  '.bcmap': 'application/octet-stream',
  '.properties': 'text/plain'
};

var defaultMimeType = 'application/octet-stream';

function WebServer() {
  this.root = '.';
  this.host = 'localhost';
  this.port = 8000;
  this.server = null;
  this.verbose = false;
  this.disableRangeRequests = false;
  this.hooks = {
    'GET': [],
    'POST': []
  };
}
WebServer.prototype = {
  start: function (callback) {
    this.server = http.createServer(this._handler.bind(this));
    this.server.listen(this.port, this.host, callback);
    console.log(
      'Server running at http://' + this.host + ':' + this.port + '/');
  },
  stop: function (callback) {
    this.server.close(callback);
    this.server = null;
  },
  _handler: function (req, res) {
    var agent = req.headers['user-agent'];
    var url = req.url;
    var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url);
    var pathPart = decodeURI(urlParts[1]), queryPart = urlParts[3];
    var verbose = this.verbose;

    var methodHooks = this.hooks[req.method];
    if (!methodHooks) {
      res.writeHead(405);
      res.end('Unsupported request method', 'utf8');
      return;
    }
    var handled = methodHooks.some(function (hook) {
      return hook(req, res);
    });
    if (handled) {
      return;
    }

    if (pathPart === '/favicon.ico') {
      fs.realpath(path.join(this.root, 'test/resources/favicon.ico'),
                  checkFile);
      return;
    }

    var disableRangeRequests = this.disableRangeRequests;

    var filePath;
    fs.realpath(path.join(this.root, pathPart), checkFile);

    function checkFile(err, file) {
      if (err) {
        res.writeHead(404);
        res.end();
        if (verbose) {
          console.error(url + ': not found');
        }
        return;
      }
      filePath = file;
      fs.stat(filePath, statFile);
    }

    var fileSize;

    function statFile(err, stats) {
      if (err) {
        res.writeHead(500);
        res.end();
        return;
      }

      fileSize = stats.size;
      var isDir = stats.isDirectory();
      if (isDir && !/\/$/.test(pathPart)) {
        res.setHeader('Location', pathPart + '/' + urlParts[2]);
        res.writeHead(301);
        res.end('Redirected', 'utf8');
        return;
      }
      if (isDir) {
        serveDirectoryIndex(filePath);
        return;
      }

      var range = req.headers['range'];
      if (range && !disableRangeRequests) {
        var rangesMatches = /^bytes=(\d+)\-(\d+)?/.exec(range);
        if (!rangesMatches) {
          res.writeHead(501);
          res.end('Bad range', 'utf8');
          if (verbose) {
            console.error(url + ': bad range: "' + range + '"');
          }
          return;
        }
        var start = +rangesMatches[1];
        var end = +rangesMatches[2];
        if (verbose) {
          console.log(url + ': range ' + start + ' - ' + end);
        }
        serveRequestedFileRange(filePath,
                                start,
                                isNaN(end) ? fileSize : (end + 1));
        return;
      }
      if (verbose) {
        console.log(url);
      }
      serveRequestedFile(filePath);
    }

    function serveDirectoryIndex(dir) {
      res.setHeader('Content-Type', 'text/html');
      res.writeHead(200);

      if (queryPart === 'frame') {
        res.end('<frameset cols=*,200><frame name=pdf>' +
          '<frame src=\"' + encodeURI(pathPart) +
          '?side\"></frameset>', 'utf8');
        return;
      }
      var all = queryPart === 'all';
      fs.readdir(dir, function (err, files) {
        if (err) {
          res.end();
          return;
        }
        res.write('
PDFs of ' + pathPart + '
\n');
        if (pathPart !== '/') {
          res.write('..
\n');
        }
        files.forEach(function (file) {
          var stat = fs.statSync(path.join(dir, file));
          var item = pathPart + file;
          if (stat.isDirectory()) {
            res.write('' +
              file + '
\n');
            return;
          }
          var ext = path.extname(file).toLowerCase();
          if (ext === '.pdf') {
            res.write('' +
              file + '
\n');
          } else if (all) {
            res.write('' +
              file + '
\n');
          }
        });
        if (files.length === 0) {
          res.write('
no files found
\n');
        }
        if (!all && queryPart !== 'side') {
          res.write('

(only PDF files are shown, ' +
            'show all)
\n');
        }
        res.end('');
      });
    }

    function serveRequestedFile(filePath) {
      var stream = fs.createReadStream(filePath, {flags: 'rs'});

      stream.on('error', function (error) {
        res.writeHead(500);
        res.end();
      });

      var ext = path.extname(filePath).toLowerCase();
      var contentType = mimeTypes[ext] || defaultMimeType;

      if (!disableRangeRequests) {
        res.setHeader('Accept-Ranges', 'bytes');
      }
      res.setHeader('Content-Type', contentType);
      res.setHeader('Content-Length', fileSize);
      res.writeHead(200);

      stream.pipe(res);
    }

    function serveRequestedFileRange(filePath, start, end) {
      var stream = fs.createReadStream(filePath, {
        flags: 'rs', start: start, end: end - 1});

      stream.on('error', function (error) {
        res.writeHead(500);
        res.end();
      });

      var ext = path.extname(filePath).toLowerCase();
      var contentType = mimeTypes[ext] || defaultMimeType;

      res.setHeader('Accept-Ranges', 'bytes');
      res.setHeader('Content-Type', contentType);
      res.setHeader('Content-Length', (end - start));
      res.setHeader('Content-Range',
        'bytes ' + start + '-' + (end - 1) + '/' + fileSize);
      res.writeHead(206);

      stream.pipe(res);
    }

  }
};

exports.WebServer = WebServer;
<|endoftext|>


<commit_before>// ==UserScript==
// @name        SO Time Lines
// @namespace   Kyll-things
// @author      Kyll
// @description Random chat improvements
// @include     *://chat.stackoverflow.com/rooms/*
// @exclude     *://chat.stackoverflow.com/rooms/info/*
// @include     *://chat.meta.stackexchange.com/rooms/*
// @exclude     *://chat.meta.stackexchange.com/rooms/info/*
// @include     *://chat.stackexchange.com/rooms/*
// @exlude      *://chat.stackexchange.com/rooms/info/*
// @version     1
// @eat         waffle
// ==/UserScript==

// Add some Plop
window.Plop = window.Plop || {}
Plopify = (...args) => Object.assign(window.Plop, ...args)

// Add some magic
HTMLCollection.prototype[Symbol.iterator] =
  HTMLCollection.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]
NodeList.prototype[Symbol.iterator] =
  NodeList.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]

// Add some style
const headNode = document.getElementsByTagName('head')[0]
    , styleNode = document.createElement('style')

headNode.appendChild(styleNode)
//TODO: Do this programmatically, kind of ugly atm
styleNode.innerHTML = '.message.two-minutes {  border-bottom-style: solid!important;  border-bottom-color: red!important;  border-bottom-width: thin!important;}.message.five-minutes {  border-bottom-style: solid!important;  border-bottom-color: green!important;  border-bottom-width: thin!important;}.message.ten-minutes {  border-bottom-style: solid!important;  border-bottom-color: blue!important;  border-bottom-width: thin!important;}'

// Add timestamps on all existing messages
// This requires the DOM to be loaded.
// Try until it worked
// If anyone has some kind of cleaner way to do this (event?), I'm taker
const allTimestampsInterval = setInterval(function tryAddAllTimestamps() {
  const allMessages = document.querySelectorAll('*[id^="message-"]')

  for(let message of allMessages) {
    let timestamp = $.data(message, 'info') && $.data(message, 'info').time
    message.setAttribute('timestamp', timestamp)
  }

  if(allMessages.length > 0) {
    clearInterval(allTimestampsInterval)
  }
}, 2000)

CHAT.addEventHandlerHook(({event_type, time_stamp, message_id}) => {
  // New message
  if(event_type === 1) {
    // We need the HTML element to exist, it's set synchronously
    // Thus, queue the query
    setTimeout(() => {
      const element = document.getElementById(`message-${message_id}`)
      if (element) {
        element.setAttribute('timestamp', time_stamp)
      }
    }, 0);
  }
})

const twoMinutesClass = 'two-minutes'
    , fiveMinutesClass = 'five-minutes'
    , tenMinutesClass = 'ten-minutes'

    , testFiveSecondsClass = 'five-seconds'

Plopify({twoMinutesClass, fiveMinutesClass, tenMinutesClass, testFiveSecondsClass})

setInterval(function manageTimeLines() {
  const removeTimeLines = () => {
    [
      ...document.getElementsByClassName(twoMinutesClass)
      ,  ...document.getElementsByClassName(fiveMinutesClass)
      ,  ...document.getElementsByClassName(tenMinutesClass)

      ,  ...document.getElementsByClassName(testFiveSecondsClass)
    ].forEach(e => {
      e.classList.remove(twoMinutesClass)
      e.classList.remove(fiveMinutesClass)
      e.classList.remove(tenMinutesClass)

      e.classList.remove(testFiveSecondsClass)
    })
  }

  const addTimeLines = () => {
    const twoMinutesToMs = 1000 * 60 * 2
        , fiveMinutesToMs = 1000 * 60 * 5
        , tenMinutesToMs = 1000 * 60 * 10

        , testFiveSecondsToMs = 1000 * 5

    Plopify({twoMinutesToMs, fiveMinutesToMs, tenMinutesToMs, testFiveSecondsToMs})

    const allMessages = document.querySelectorAll('.message')
        , maxLookupAmount = Math.min(42, allMessages.length)
        , now = Date.now()

    Plopify({maxLookupAmount})

    let markedTwoMinutes = false
      , markedFiveMinutes = false
      , markedTenMinutes = false

      , testMarkedFiveSeconds = false

    for(i = allMessages.length - 1; i > allMessages.length - maxLookupAmount; i--) {
      let currentMessage = allMessages[i]
        // Fetch the timestamp on the HTML element, multiply by 1000 to get
        // millisecond precision
        , messageTimestamp = Number.parseInt(
          currentMessage.getAttribute('timestamp')
        ) * 1000

      if (
        messageTimestamp < (now - testFiveSecondsToMs)
        && !testMarkedFiveSeconds
      ) {
        currentMessage.classList.add(testFiveSecondsClass)
        testMarkedFiveSeconds = true
      }
      if (
        messageTimestamp < (now - twoMinutesToMs)
        && !markedTwoMinutes
      ) {
        currentMessage.classList.add(twoMinutesClass)
        markedTwoMinutes = true
      }
      if (
        messageTimestamp < (now - fiveMinutesToMs)
        && !markedFiveMinutes
      ) {
        currentMessage.classList.add(fiveMinutesClass)
        markedFiveMinutes = true
      }
      if (
        messageTimestamp < (now - tenMinutesToMs)
        && !markedTenMinutes
      ) {
        currentMessage.classList.add(tenMinutesClass)
        markedTenMinutes = true
      }
    }
  }

  removeTimeLines()

  addTimeLines()
}, 4200)
<commit_msg>Line management process rewrite<commit_after>// ==UserScript==
// @name        SO Time Lines
// @namespace   Kyll-things
// @author      Kyll
// @description Random chat improvements
// @include     *://chat.stackoverflow.com/rooms/*
// @exclude     *://chat.stackoverflow.com/rooms/info/*
// @include     *://chat.meta.stackexchange.com/rooms/*
// @exclude     *://chat.meta.stackexchange.com/rooms/info/*
// @include     *://chat.stackexchange.com/rooms/*
// @exlude      *://chat.stackexchange.com/rooms/info/*
// @version     1
// @eat         waffle
// ==/UserScript==

// Add some Plop
window.Plop = window.Plop || {}
Plopify = (...args) => Object.assign(window.Plop, ...args)

// Add some magic
HTMLCollection.prototype[Symbol.iterator] =
  HTMLCollection.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]
NodeList.prototype[Symbol.iterator] =
  NodeList.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]

// Add some style
const headNode = document.getElementsByTagName('head')[0]
    , linesStyleNode = document.createElement('style')

headNode.appendChild(linesStyleNode)

// Add timestamps on all existing messages
// This requires the DOM to be loaded.
// Try until it worked
// If anyone has some kind of cleaner way to do this (event?), I'm taker
const allTimestampsInterval = setInterval(function tryAddAllTimestamps() {
  const allMessages = document.querySelectorAll('*[id^="message-"]')

  for(let message of allMessages) {
    let timestamp = $.data(message, 'info') && $.data(message, 'info').time
    message.setAttribute('timestamp', timestamp)
  }

  if(allMessages.length > 0) {
    clearInterval(allTimestampsInterval)
  }
}, 2000)

CHAT.addEventHandlerHook(({event_type, time_stamp, message_id}) => {
  // New message
  if(event_type === 1) {
    // We need the HTML element to exist, it's set synchronously
    // Thus, queue the query
    setTimeout(() => {
      const element = document.getElementById(`message-${message_id}`)
      if (element) {
        element.setAttribute('timestamp', time_stamp)
      }
    }, 0)
  }
})

const makeLine = (colour, durationSeconds, className) =>
  ({ colour, durationSeconds, className })

// Sequence ordered by duration in ascending order
// Example:
// [
//   { colour: 'red', duration: 120, className: 'two-minutes'}
// ]
const lines = []
lines.maxLookupAmount = 42
lines.addLine = (colour, durationSeconds, className, doUpdate = true) => {
  const newLine = makeLine(colour, durationSeconds, className)
      , indexToInsert = lines.findIndex(e => e.durationSeconds > durationSeconds)

  if(indexToInsert === -1) {
    lines.push(newLine)
  }
  else {
    lines.splice(
      indexToInsert,
      0,
      newLine
    )
  }
  if(doUpdate) {
    lines.updateCSS()
  }
}

lines.addMultipleLines = (...linesSequence) => {
  linesSequence.forEach(line => lines.addLine(
    line.colour, line.durationSeconds, line.className, false
  ))
  lines.updateCSS()
}

lines.CSSNode = linesStyleNode
lines.generateCSS = () => lines.reduce((summedCSS, line) =>
  `${summedCSS}
  .message.${line.className} {
    border-bottom-style: solid!important;
    border-bottom-color: ${line.colour}!important;
      border-bottom-width: thin!important;
  }`, '')
lines.updateCSS = () => {
  lines.CSSNode.innerHTML = lines.generateCSS()
}

// Default lines
if(lines.length === 0) {
  lines.addMultipleLines({
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  })
}

Plopify({ lines })

setInterval(function manageTimeLines() {
  const removeAllLines = () => {
    // TODO: Implement Timeouts instead of raw removals
    const allClasses = lines.map(line => line.className)
    // Ninja it to array because element collections suck
    allClasses
      .map(className => document.getElementsByClassName(className))
      .forEach(elementsCollection =>
        [...elementsCollection].forEach(element =>
          element.classList.remove(...allClasses)
        )
      )
  }

  const addLines = () => {
    const allMessages = document.querySelectorAll('.message')
        , maxLookupAmount = Math.min(lines.maxLookupAmount, allMessages.length)
        , now = Date.now()

        , thresholds = [...lines]

    for(let i = allMessages.length - 1; i > allMessages.length - maxLookupAmount; i--) {
      const currentLine = thresholds[0]
          , currentMessage = allMessages[i]
          , messageTimestamp = Number.parseInt(
            currentMessage.getAttribute('timestamp')
          ) * 1000

      if(!currentLine) {
        // No more lines to add, quit
        break
      }

      if(
        messageTimestamp < (now - (currentLine.durationSeconds * 1000))
      ) {
        currentMessage.classList.add(currentLine.className)

        // We're done with this line, remove it
        thresholds.shift()
        // A message can have multiple lines attached, so redo this round
        i++
      }
    }
  }

  removeAllLines()

  addLines()
}, 420)
<|endoftext|>


<commit_before>// ==UserScript==
// @name        SO Time Lines
// @namespace   Kyll-things
// @author      Kyll
// @description Random chat improvements
// @include     *://chat.stackoverflow.com/rooms/*
// @exclude     *://chat.stackoverflow.com/rooms/info/*
// @include     *://chat.meta.stackexchange.com/rooms/*
// @exclude     *://chat.meta.stackexchange.com/rooms/info/*
// @include     *://chat.stackexchange.com/rooms/*
// @exlude      *://chat.stackexchange.com/rooms/info/*
// @version     1.1
// @eat         waffle
// ==/UserScript==

// Add some Plop
window.Plop = window.Plop || {}
Plopify = (...args) => Object.assign(window.Plop, ...args)

// Add some magic
HTMLCollection.prototype[Symbol.iterator] =
  HTMLCollection.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]
NodeList.prototype[Symbol.iterator] =
  NodeList.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]

// Add some style
const headNode = document.getElementsByTagName('head')[0]
    , linesStyleNode = document.createElement('style')

headNode.appendChild(linesStyleNode)

// Add timestamps on all existing messages
// This requires the DOM to be loaded.
// Try until it worked
// If anyone has some kind of cleaner way to do this (event?), I'm taker
const allTimestampsInterval = setInterval(function tryAddAllTimestamps() {
  const allMessages = document.querySelectorAll('*[id^="message-"]')

  for(let message of allMessages) {
    let timestamp = $.data(message, 'info') && $.data(message, 'info').time
    message.setAttribute('timestamp', timestamp)
  }

  if(allMessages.length > 0) {
    clearInterval(allTimestampsInterval)
  }
}, 2000)

CHAT.addEventHandlerHook(({event_type, time_stamp, message_id}) => {
  // New message
  if(event_type === 1) {
    // We need the HTML element to exist, it's set synchronously
    // Thus, queue the query
    setTimeout(() => {
      const element = document.getElementById(`message-${message_id}`)
      if (element) {
        element.setAttribute('timestamp', time_stamp)
      }
    }, 0)
  }
})

const makeLine = (colour, durationSeconds, className) =>
  ({ colour, durationSeconds, className })

// Sequence ordered by duration in ascending order
// Example:
// [
//   { colour: 'red', duration: 120, className: 'two-minutes'}
// ]
const lines = []
lines.maxLookupAmount = 42
lines.addLine = (colour, durationSeconds, className, doUpdate = true) => {
  const newLine = makeLine(colour, durationSeconds, className)
      , indexToInsert = lines.findIndex(e => e.durationSeconds > durationSeconds)

  if(indexToInsert === -1) {
    lines.push(newLine)
  }
  else {
    lines.splice(
      indexToInsert,
      0,
      newLine
    )
  }
  if(doUpdate) {
    lines.updateCSS()
  }
}

lines.addMultipleLines = (...linesSequence) => {
  linesSequence.forEach(line => lines.addLine(
    line.colour, line.durationSeconds, line.className, false
  ))
  lines.updateCSS()
}

lines.CSSNode = linesStyleNode
lines.generateCSS = () => lines.reduce((summedCSS, line) =>
  `${summedCSS}
  .message.${line.className} {
    border-bottom-style: solid!important;
    border-bottom-color: ${line.colour}!important;
      border-bottom-width: thin!important;
  }`, '')
lines.updateCSS = () => {
  lines.CSSNode.innerHTML = lines.generateCSS()
}

// Default lines
if(lines.length === 0) {
  lines.addMultipleLines({
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  })
}

Plopify({ lines })

setInterval(function manageTimeLines() {
  const removeAllLines = () => {
    // TODO: Implement Timeouts instead of raw removals
    const allClasses = lines.map(line => line.className)
    // Ninja it to array because element collections suck
    allClasses
      .map(className => document.getElementsByClassName(className))
      .forEach(elementsCollection =>
        [...elementsCollection].forEach(element =>
          element.classList.remove(...allClasses)
        )
      )
  }

  const addLines = () => {
    const allMessages = document.querySelectorAll('.message')
        , maxLookupAmount = Math.min(lines.maxLookupAmount, allMessages.length)
        , now = Date.now()

        , thresholds = [...lines]

    for(let i = allMessages.length - 1; i > allMessages.length - maxLookupAmount; i--) {
      const currentLine = thresholds[0]
          , currentMessage = allMessages[i]
          , messageTimestamp = Number.parseInt(
            currentMessage.getAttribute('timestamp')
          ) * 1000

      if(!currentLine) {
        // No more lines to add, quit
        break
      }

      if(
        messageTimestamp < (now - (currentLine.durationSeconds * 1000))
      ) {
        currentMessage.classList.add(currentLine.className)

        // We're done with this line, remove it
        thresholds.shift()
        // A message can have multiple lines attached, so redo this round
        i++
      }
    }
  }

  removeAllLines()

  addLines()
}, 420)
<commit_msg>Simplified code<commit_after>// ==UserScript==
// @name        SO Time Lines
// @namespace   Kyll-things
// @author      Kyll
// @description Random chat improvements
// @include     *://chat.stackoverflow.com/rooms/*
// @exclude     *://chat.stackoverflow.com/rooms/info/*
// @include     *://chat.meta.stackexchange.com/rooms/*
// @exclude     *://chat.meta.stackexchange.com/rooms/info/*
// @include     *://chat.stackexchange.com/rooms/*
// @exlude      *://chat.stackexchange.com/rooms/info/*
// @version     1.1
// @eat         waffle
// ==/UserScript==

// Add some Plop
window.Plop = window.Plop || {}
Plopify = (...args) => Object.assign(window.Plop, ...args)

// Add some magic
HTMLCollection.prototype[Symbol.iterator] =
  HTMLCollection.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]
NodeList.prototype[Symbol.iterator] =
  NodeList.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]

// Add some style
const linesStyleNode = document.createElement('style')
document.head.appendChild(linesStyleNode)

// Add timestamps on all existing messages
// This requires the DOM to be loaded.
// Try until it worked
// If anyone has some kind of cleaner way to do this (event?), I'm taker
const allTimestampsInterval = setInterval(function tryAddAllTimestamps() {
  const allMessages = document.querySelectorAll('*[id^="message-"]')

  for(let message of allMessages) {
    let timestamp = $.data(message, 'info') && $.data(message, 'info').time
    message.setAttribute('timestamp', timestamp)
  }

  if(allMessages.length > 0) {
    clearInterval(allTimestampsInterval)
  }
}, 2000)

CHAT.addEventHandlerHook(({event_type, time_stamp, message_id}) => {
  // New message
  if(event_type === 1) {
    // We need the HTML element to exist, it's set synchronously
    // Thus, queue the query
    setTimeout(() => {
      const element = document.getElementById(`message-${message_id}`)
      if (element) {
        element.setAttribute('timestamp', time_stamp)
      }
    }, 0)
  }
})

const makeLine = (colour, durationSeconds, className) =>
  ({ colour, durationSeconds, className })

// Sequence ordered by duration in ascending order
// Example:
// [
//   { colour: 'red', duration: 120, className: 'two-minutes'}
// ]
const lines = []
lines.maxLookupAmount = 42
lines.addLine = (colour, durationSeconds, className, doUpdate = true) => {
  const newLine = makeLine(colour, durationSeconds, className)
      , indexToInsert = lines.findIndex(e => e.durationSeconds > durationSeconds)

  if(indexToInsert === -1) {
    lines.push(newLine)
  }
  else {
    lines.splice(
      indexToInsert,
      0,
      newLine
    )
  }
  if(doUpdate) {
    lines.updateCSS()
  }
}

lines.addMultipleLines = (...linesSequence) => {
  linesSequence.forEach(line => lines.addLine(
    line.colour, line.durationSeconds, line.className, false
  ))
  lines.updateCSS()
}

lines.CSSNode = linesStyleNode
lines.generateCSS = () => lines.reduce((summedCSS, line) =>
  `${summedCSS}
  .message.${line.className} {
    border-bottom-style: solid!important;
    border-bottom-color: ${line.colour}!important;
      border-bottom-width: thin!important;
  }`, '')
lines.updateCSS = () => {
  lines.CSSNode.innerHTML = lines.generateCSS()
}

// Default lines
if(lines.length === 0) {
  lines.addMultipleLines({
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  })
}

Plopify({ lines })

setInterval(function manageTimeLines() {
  const removeAllLines = () => {
    // TODO: Implement Timeouts instead of raw removals
    const allClasses = lines.map(line => line.className)
    // Ninja it to array because element collections suck
    allClasses
      .map(className => document.getElementsByClassName(className))
      .forEach(elementsCollection =>
        [...elementsCollection].forEach(element =>
          element.classList.remove(...allClasses)
        )
      )
  }

  const addLines = () => {
    const allMessages = document.querySelectorAll('.message')
        , maxLookupAmount = Math.min(lines.maxLookupAmount, allMessages.length)
        , now = Date.now()

        , thresholds = [...lines]

    for(let i = allMessages.length - 1; i > allMessages.length - maxLookupAmount; i--) {
      const currentLine = thresholds[0]
          , currentMessage = allMessages[i]
          , messageTimestamp = Number.parseInt(
            currentMessage.getAttribute('timestamp')
          ) * 1000

      if(!currentLine) {
        // No more lines to add, quit
        break
      }

      if(
        messageTimestamp < (now - (currentLine.durationSeconds * 1000))
      ) {
        currentMessage.classList.add(currentLine.className)

        // We're done with this line, remove it
        thresholds.shift()
        // A message can have multiple lines attached, so redo this round
        i++
      }
    }
  }

  removeAllLines()

  addLines()
}, 420)
<|endoftext|>


<commit_before>// ==UserScript==
// @name        SO Time Lines
// @namespace   Kyll-things
// @author      Kyll
// @description Random chat improvements
// @include     *://chat.stackoverflow.com/rooms/*
// @exclude     *://chat.stackoverflow.com/rooms/info/*
// @include     *://chat.meta.stackexchange.com/rooms/*
// @exclude     *://chat.meta.stackexchange.com/rooms/info/*
// @include     *://chat.stackexchange.com/rooms/*
// @exlude      *://chat.stackexchange.com/rooms/info/*
// @version     1.1
// @eat         waffle
// ==/UserScript==

// Add some Plop
window.Plop = window.Plop || {}
Plopify = (...args) => Object.assign(window.Plop, ...args)

// Add some magic
HTMLCollection.prototype[Symbol.iterator] =
  HTMLCollection.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]
NodeList.prototype[Symbol.iterator] =
  NodeList.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]

// Add some style
const linesStyleNode = document.createElement('style')
document.head.appendChild(linesStyleNode)

// Add timestamps on all existing messages
// This requires the DOM to be loaded.
// Try until it worked
// If anyone has some kind of cleaner way to do this (event?), I'm taker
const allTimestampsInterval = setInterval(function tryAddAllTimestamps() {
  const allMessages = document.querySelectorAll('*[id^="message-"]')

  for(let message of allMessages) {
    let timestamp = $.data(message, 'info') && $.data(message, 'info').time
    message.setAttribute('timestamp', timestamp)
  }

  if(allMessages.length > 0) {
    clearInterval(allTimestampsInterval)
  }
}, 2000)

CHAT.addEventHandlerHook(({event_type, time_stamp, message_id}) => {
  // New message
  if(event_type === 1) {
    // We need the HTML element to exist, it's set synchronously
    // Thus, queue the query
    setTimeout(() => {
      const element = document.getElementById(`message-${message_id}`)
      if (element) {
        element.setAttribute('timestamp', time_stamp)
      }
    }, 0)
  }
})

const makeLine = (colour, durationSeconds, className) =>
  ({ colour, durationSeconds, className })

// Sequence ordered by duration in ascending order
// Example:
// [
//   { colour: 'red', duration: 120, className: 'two-minutes'}
// ]
const lines = []
lines.maxLookupAmount = 42
lines.addLine = (colour, durationSeconds, className, doUpdate = true) => {
  const newLine = makeLine(colour, durationSeconds, className)
      , indexToInsert = lines.findIndex(e => e.durationSeconds > durationSeconds)

  if(indexToInsert === -1) {
    lines.push(newLine)
  }
  else {
    lines.splice(
      indexToInsert,
      0,
      newLine
    )
  }
  if(doUpdate) {
    lines.updateCSS()
  }
}

lines.addMultipleLines = (...linesSequence) => {
  linesSequence.forEach(line => lines.addLine(
    line.colour, line.durationSeconds, line.className, false
  ))
  lines.updateCSS()
}

lines.CSSNode = linesStyleNode
lines.generateCSS = () => lines.reduce((summedCSS, line) =>
  `${summedCSS}
  .message.${line.className} {
    border-bottom-style: solid!important;
    border-bottom-color: ${line.colour}!important;
      border-bottom-width: thin!important;
  }`, '')
lines.updateCSS = () => {
  lines.CSSNode.innerHTML = lines.generateCSS()
}

// Default lines
if(lines.length === 0) {
  lines.addMultipleLines({
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  })
}

Plopify({ lines })

setInterval(function manageTimeLines() {
  const removeAllLines = () => {
    // TODO: Implement Timeouts instead of raw removals
    const allClasses = lines.map(line => line.className)
    // Ninja it to array because element collections suck
    allClasses
      .map(className => document.getElementsByClassName(className))
      .forEach(elementsCollection =>
        [...elementsCollection].forEach(element =>
          element.classList.remove(...allClasses)
        )
      )
  }

  const addLines = () => {
    const allMessages = document.querySelectorAll('.message')
        , maxLookupAmount = Math.min(lines.maxLookupAmount, allMessages.length)
        , now = Date.now()

        , thresholds = [...lines]

    for(let i = allMessages.length - 1; i > allMessages.length - maxLookupAmount; i--) {
      const currentLine = thresholds[0]
          , currentMessage = allMessages[i]
          , messageTimestamp = Number.parseInt(
            currentMessage.getAttribute('timestamp')
          ) * 1000

      if(!currentLine) {
        // No more lines to add, quit
        break
      }

      if(
        messageTimestamp < (now - (currentLine.durationSeconds * 1000))
      ) {
        currentMessage.classList.add(currentLine.className)

        // We're done with this line, remove it
        thresholds.shift()
        // A message can have multiple lines attached, so redo this round
        i++
      }
    }
  }

  removeAllLines()

  addLines()
}, 420)
<commit_msg>Cleaned up, added persistence, version bump<commit_after>// ==UserScript==
// @name        SO Time Lines
// @namespace   Kyll-things
// @author      Kyll
// @description Random chat improvements
// @include     *://chat.stackoverflow.com/rooms/*
// @exclude     *://chat.stackoverflow.com/rooms/info/*
// @include     *://chat.meta.stackexchange.com/rooms/*
// @exclude     *://chat.meta.stackexchange.com/rooms/info/*
// @include     *://chat.stackexchange.com/rooms/*
// @exlude      *://chat.stackexchange.com/rooms/info/*
// @version     1.2
// @eat         waffle
// ==/UserScript==

// Add some Plop
window.Plop = window.Plop || {}
Plopify = (...args) => Object.assign(window.Plop, ...args)

const storeKey = 'plop-lines'

// Add some magic
HTMLCollection.prototype[Symbol.iterator] =
  HTMLCollection.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]
NodeList.prototype[Symbol.iterator] =
  NodeList.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]

// Add some style
const linesStyleNode = document.createElement('style')
document.head.appendChild(linesStyleNode)

// Add timestamps on all existing messages
// This requires the DOM to be loaded.
// Try until it worked
// If anyone has some kind of cleaner way to do this (event?), I'm taker
const allTimestampsInterval = setInterval(function tryAddAllTimestamps() {
  const allMessages = document.querySelectorAll('*[id^="message-"]')

  for(let message of allMessages) {
    let timestamp = $.data(message, 'info') && $.data(message, 'info').time
    message.setAttribute('timestamp', timestamp)
  }

  if(allMessages.length > 0) {
    clearInterval(allTimestampsInterval)
  }
}, 2000)

CHAT.addEventHandlerHook(({event_type, time_stamp, message_id}) => {
  // New message
  if(event_type === 1) {
    // We need the HTML element to exist, it's set synchronously
    // Thus, queue the query
    setTimeout(() => {
      const element = document.getElementById(`message-${message_id}`)
      if (element) {
        element.setAttribute('timestamp', time_stamp)
      }
    }, 0)
  }
})

const makeLine = (colour, durationSeconds, className) =>
  ({ colour, durationSeconds, className })

// Sequence ordered by duration in ascending order
// Example:
// [
//   { colour: 'red', duration: 120, className: 'two-minutes'}
// ]
const lines = []

const defaultLines = [
  {
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  }
]

Object.defineProperties(lines, {
  addLine : {
    value: (colour, durationSeconds, className, doUpdate = true) => {
      const newLine = makeLine(colour, durationSeconds, className)
          , indexToInsert = lines.findIndex(e => e.durationSeconds > durationSeconds)

      if(indexToInsert === -1) {
        lines.push(newLine)
      }
      else {
        lines.splice(
          indexToInsert,
          0,
          newLine
        )
      }
      if(doUpdate) {
        lines.updateCSS()
      }
    }
  },
  addMultipleLines: {
    value: (...linesSequence) => {
      linesSequence.forEach(line => lines.addLine(
        line.colour, line.durationSeconds, line.className, false
      ))
      lines.updateCSS()
    }
  },
  removeLine: {
    value: (index, doUpdate = true) => {
      lines.splice(index, 1)
      if(doUpdate) {
        lines.updateCSS()
      }
    }
  },
  removeMultipleLines: {
    value: (...indexes) => {
      indexes.forEach(index => lines.removeLine(index), false)
      lines.updateCSS()
    }
  },
  maxLookupAmount: {
    value: 42
  },
  CSSNode: {
    value: linesStyleNode
  },
  generateCSS: {
    value: () => lines.reduce((summedCSS, line) =>
      `${summedCSS}
      .message.${line.className} {
        border-bottom-style: solid!important;
        border-bottom-color: ${line.colour}!important;
          border-bottom-width: thin!important;
      }`, '')
  },
  updateCSS: {
    value: () => lines.CSSNode.innerHTML = lines.generateCSS()
  },
  save: {
    value: () => localStorage.setItem('plop-lines', JSON.stringify([...lines]))
  },
  reset: {
    value: () => {
      localStorage.removeItem(storeKey)
      lines.removeMultipleLines(
        ...lines.keys()
      )
      lines.addMultipleLines(...defaultLines)
    }
  }
})

// TODO: That's overstepping the responsibility of this script.
// Need to hoist the "help" thingies in its own script and require it
Plop.help = () => console.log('Maybe you meant `Plop.halp()`?')
Plop.halp = () => console.log(
`Plop!
You can view the lines properties by typing Plop.lines in this console.
You can change any property of said lines with your debug tools or a command.
The colours must be CSS colours, such as "purple" or "0xFAFAFA"
Example:
    Plop.lines[0].colour = 'purple'
    Plop.lines[3].durationSeconds = 1200 // 20 minutes

To add a new line, use the Plop.lines.addLine function.
First argument is the colour of the line.
Second argument is the duration in seconds it represents.
Third argument is the CSS class name it should use.
Example:
    Plop.lines.addLine('yellow', 180 /* three minutes */, 'three-minutes')

To remove a line, use the Plop.lines.removeLine function.
Only argument is the index (can be negative as reverse index).
Example:
    Plop.lines.removeLine(0)  // Remove first line
    Plop.lines.removeLine(-1) // Remove last line

Once you've done your changes, save them so that they persist across reloads:
    Plop.lines.save()

If you screwed up:
    Plop.reset()

Have fun!
 - Kyll
`
)

const storedLines = JSON.parse(localStorage.getItem(storeKey))

if(storedLines) {
  lines.addMultipleLines(...storedLines)
}
else {
  // Default lines
  lines.addMultipleLines({
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  })
}

Plopify({ lines })

setInterval(function manageTimeLines() {
  const removeAllLines = () => {
    // TODO: Implement Timeouts instead of raw removals
    const allClasses = lines.map(line => line.className)
    // Ninja it to array because element collections suck
    allClasses
      .map(className => document.getElementsByClassName(className))
      .forEach(elementsCollection =>
        [...elementsCollection].forEach(element =>
          element.classList.remove(...allClasses)
        )
      )
  }

  const addLines = () => {
    const allMessages = document.querySelectorAll('.message')
        , maxLookupAmount = Math.min(lines.maxLookupAmount, allMessages.length)
        , now = Date.now()

        , thresholds = [...lines]

    for(let i = allMessages.length - 1; i > allMessages.length - maxLookupAmount; i--) {
      const currentLine = thresholds[0]
          , currentMessage = allMessages[i]
          , messageTimestamp = Number.parseInt(
            currentMessage.getAttribute('timestamp')
          ) * 1000

      if(!currentLine) {
        // No more lines to add, quit
        break
      }

      if(
        messageTimestamp < (now - (currentLine.durationSeconds * 1000))
      ) {
        currentMessage.classList.add(currentLine.className)

        // We're done with this line, remove it
        thresholds.shift()
        // A message can have multiple lines attached, so redo this round
        i++
      }
    }
  }

  removeAllLines()

  addLines()
}, 420)
<|endoftext|>


<commit_before>import React from 'react';
import ReactDOM from 'react-dom';
import io from 'socket.io-client';
import Geohash from './latlon-geohash.js';

let geohash = null;
let parts = window.location.pathname.split('/');

const socket = io({ path: parts.slice(0, parts.length - 2).join('/') + '/socket.io' });

socket.on('player-number', function (number) {
    $('#player').text(`You are player ${number}`).addClass(`player-${number}`)
});

socket.on('chat-message', function (data) {
    $('
' + playerName(data.player) +
        ': ' + data.message + '
').appendTo($('#chat'));
    scrollChat();
});

socket.on('chat-status', function (data) {
    $('#chat').append(
        '
' + playerName(data.player) + ' ' + data.message + '
');
    scrollChat();
});

socket.on('state', function (game) {
    let board = document.getElementById('board');
    if (board) {
        drawBoard(board, game.columns, game.rows, game.board);
        $('#numToWin').text(game.toWin);
    }
    if (game.state === 'won') {
        $('#status').html(`${playerName(game.turn)} wins!`);
        highlightWinningCells(game.winning, $('#board'));
    } else if (game.state === 'draw') {
        $('#status').html('Draw!');
    } else if (game.state === 'waiting') {
        $('#status').html('Waiting for opponent to join');
    } else if (game.state === 'active') {
        $('#status').html(`${playerName(game.turn)}'s turn`);
    }

    $('#chat-status').text(game.connected + ' users connected');
});

socket.on('place-token', function (data) {
    $('#status').html(`${playerName(data.turn)}'s turn`);
});

function getGeohash(callback) {
    if (geohash) {
        callback(geohash);
    } else {
        navigator.geolocation.getCurrentPosition(function (pos) {
            geohash = Geohash.encode(pos.coords.latitude, pos.coords.longitude, 9);
            callback(geohash);
        });
    }
}

function showSliderValue(slider) {
    slider.parent().find('span').text(slider.val());
}

function Column(props) {
    let cells = [];
    for (let i = 0; i < props.rows; i++) {
        cells.push(<Cell key={i} player={props.cells[i]}/>);
    }
    return (

            {cells}

    );
}

function Cell(props) {
    let classNames = 'cell';
    if (props.player) {
        classNames += (' player-' + props.player);
    }
    return (

    );
}

class Board extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            board: props.board || (new Array(parseInt(props.cols)).fill([]))
        };
        socket.on('place-token',(data) => {
            this.state.board[data.column].push(data.player.number);
            this.setState({ board: this.state.board });
        });
    }

    handleClick(i) {
    }

    render() {
        let colList = [];

        for (let i = 0; i < this.props.cols; i++) {
            colList.push(<Column rows={this.props.rows} key={i} cells={this.state.board[i]} />);
        }

        return (

                {colList}

        );
    }
}

Board.defaultProps = { cols: 7, rows: 6, board: [] };

function drawBoard (element, cols, rows, board, winning) {
    ReactDOM.render(
        <Board rows={rows} cols={cols} board={board}/>,
        element
    );
}

function highlightWinningCells(winning, element) {
    winning.forEach((c) => {
        element.find(`.column:eq(${c[0]}) .cell:eq(${c[1]})`).addClass('winning');
    });
}

function playerName(player) {
    return '<span class="player-name'+(player.number ? ` player-${player.number}` : '') +'">' + player.name + '';
}

function scrollChat() {
    var e = $('#chat');
    e.scrollTop(e[0].scrollHeight);
}

function connect(name, id) {
    socket.emit('join', { id: id, name: name });
    $('#name-modal').modal('hide');
}

$(document).ready(function () {
    let match = window.location.href.match(/\/game\/([a-z0-9]+)/);
    if (match) {
        $('#name-modal').modal('show');
        let name = $('#name-input').val();
        let id = match[1];

        $('#connect').click(function () { connect(name, id) });
        $('#name-input').keyup(function (e) {
            if (e.keyCode == 13) {
                connect(name, id);
            }
        });

        $('#board').on('click', '.column', function () {
            socket.emit('place-token', $(this).index());
        });

        $('#chat-input').keyup(function (e) {
            if (e.keyCode == 13) {
                socket.emit('chat-message', $(this).val());
                $(this).val('');
            }
        });
    } else {
        $('input[type=range]').on('input', function () {
            showSliderValue($(this));
        }).each(function () {
            showSliderValue($(this));
        });

        if (navigator.geolocation) {
            $('#local-games-container').show();
            // Fetch "nearby" games
            $('#find-local-games').click(function () {
                getGeohash(function (geohash) {
                    $.ajax({
                        url: 'games/' + geohash,
                        dataType: 'json',
                        success: function (data) {
                            $('#local-games').html('');
                            if (data.length) {
                                data.forEach(function (data) {
                                    if (data.game.player1) {
                                        var el = $('' +
                                            playerName(data.game.player1) +
                                            (data.game.player2 ? ' vs. ' + playerName(data.game.player2) : "'s game") +
                                            '');
                                        var board = $('

');
                                        el.appendTo($('#local-games'));
                                        board.appendTo(el);
                                        drawBoard(board, data.game.columns, data.game.rows, data.game.board);
                                    }
                                });
                            } else {
                                $('#local-games').text('No games found');
                            }
                        }
                    });
                });
                return false;
            });

            $('#create-geolocated-game').show().click(function () {
                getGeohash(function (geohash) {
                    $('#geohash').val(geohash);
                    document.getElementById('form').submit();
                });
                return false;
            });

            $('#create-game').show().click(function () {
                $('#geohash').val('');

                return true;
            });
        }

        // Show recent game results
        $.ajax({
            url: 'results',
            dataType: 'json',
            success: function (data) {
                $('#recent-results').html('');
                if (data.length) {
                    data.forEach(function (result) {
                        var el = $('
' +
                            playerName(result.winner) + (result.draw ? ' drew against ' : ' beat ') +
                            playerName(result.loser) + '
');
                        var board = $('

');
                        el.appendTo($('#recent-results'));
                        board.appendTo(el);
                        drawBoard(board[0], result.columns, result.rows, result.board);
                        if (!result.draw) {
                            highlightWinningCells(result.winning, board);
                        }
                    });
                } else {
                    $('#recent-results').text('No recent results');
                }
            }
        });
    }
});
<commit_msg>Duplicate the board when changing state<commit_after>import React from 'react';
import ReactDOM from 'react-dom';
import io from 'socket.io-client';
import Geohash from './latlon-geohash.js';

let geohash = null;
let parts = window.location.pathname.split('/');

const socket = io({ path: parts.slice(0, parts.length - 2).join('/') + '/socket.io' });

socket.on('player-number', function (number) {
    $('#player').text(`You are player ${number}`).addClass(`player-${number}`)
});

socket.on('chat-message', function (data) {
    $('
' + playerName(data.player) +
        ': ' + data.message + '
').appendTo($('#chat'));
    scrollChat();
});

socket.on('chat-status', function (data) {
    $('#chat').append(
        '
' + playerName(data.player) + ' ' + data.message + '
');
    scrollChat();
});

socket.on('state', function (game) {
    let board = document.getElementById('board');
    if (board) {
        drawBoard(board, game.columns, game.rows, game.board);
        $('#numToWin').text(game.toWin);
    }
    if (game.state === 'won') {
        $('#status').html(`${playerName(game.turn)} wins!`);
        highlightWinningCells(game.winning, $('#board'));
    } else if (game.state === 'draw') {
        $('#status').html('Draw!');
    } else if (game.state === 'waiting') {
        $('#status').html('Waiting for opponent to join');
    } else if (game.state === 'active') {
        $('#status').html(`${playerName(game.turn)}'s turn`);
    }

    $('#chat-status').text(game.connected + ' users connected');
});

socket.on('place-token', function (data) {
    $('#status').html(`${playerName(data.turn)}'s turn`);
});

function getGeohash(callback) {
    if (geohash) {
        callback(geohash);
    } else {
        navigator.geolocation.getCurrentPosition(function (pos) {
            geohash = Geohash.encode(pos.coords.latitude, pos.coords.longitude, 9);
            callback(geohash);
        });
    }
}

function showSliderValue(slider) {
    slider.parent().find('span').text(slider.val());
}

function Column(props) {
    let cells = [];
    for (let i = 0; i < props.rows; i++) {
        cells.push(<Cell key={i} player={props.cells[i]}/>);
    }
    return (

            {cells}

    );
}

function Cell(props) {
    let classNames = 'cell';
    if (props.player) {
        classNames += (' player-' + props.player);
    }
    return (

    );
}

class Board extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            board: props.board || (new Array(parseInt(props.cols)).fill([]))
        };
        socket.on('place-token',(data) => {
            let board = this.state.board.slice();
            board[data.column].push(data.player.number);
            this.setState({ board: board });
        });
    }

    handleClick(i) {
    }

    render() {
        let colList = [];

        for (let i = 0; i < this.props.cols; i++) {
            colList.push(<Column rows={this.props.rows} key={i} cells={this.state.board[i]} />);
        }

        return (

                {colList}

        );
    }
}

Board.defaultProps = { cols: 7, rows: 6, board: [] };

function drawBoard (element, cols, rows, board, winning) {
    ReactDOM.render(
        <Board rows={rows} cols={cols} board={board}/>,
        element
    );
}

function highlightWinningCells(winning, element) {
    winning.forEach((c) => {
        element.find(`.column:eq(${c[0]}) .cell:eq(${c[1]})`).addClass('winning');
    });
}

function playerName(player) {
    return '<span class="player-name'+(player.number ? ` player-${player.number}` : '') +'">' + player.name + '';
}

function scrollChat() {
    var e = $('#chat');
    e.scrollTop(e[0].scrollHeight);
}

function connect(name, id) {
    socket.emit('join', { id: id, name: name });
    $('#name-modal').modal('hide');
}

$(document).ready(function () {
    let match = window.location.href.match(/\/game\/([a-z0-9]+)/);
    if (match) {
        $('#name-modal').modal('show');
        let name = $('#name-input').val();
        let id = match[1];

        $('#connect').click(function () { connect(name, id) });
        $('#name-input').keyup(function (e) {
            if (e.keyCode == 13) {
                connect(name, id);
            }
        });

        $('#board').on('click', '.column', function () {
            socket.emit('place-token', $(this).index());
        });

        $('#chat-input').keyup(function (e) {
            if (e.keyCode == 13) {
                socket.emit('chat-message', $(this).val());
                $(this).val('');
            }
        });
    } else {
        $('input[type=range]').on('input', function () {
            showSliderValue($(this));
        }).each(function () {
            showSliderValue($(this));
        });

        if (navigator.geolocation) {
            $('#local-games-container').show();
            // Fetch "nearby" games
            $('#find-local-games').click(function () {
                getGeohash(function (geohash) {
                    $.ajax({
                        url: 'games/' + geohash,
                        dataType: 'json',
                        success: function (data) {
                            $('#local-games').html('');
                            if (data.length) {
                                data.forEach(function (data) {
                                    if (data.game.player1) {
                                        var el = $('' +
                                            playerName(data.game.player1) +
                                            (data.game.player2 ? ' vs. ' + playerName(data.game.player2) : "'s game") +
                                            '');
                                        var board = $('

');
                                        el.appendTo($('#local-games'));
                                        board.appendTo(el);
                                        drawBoard(board, data.game.columns, data.game.rows, data.game.board);
                                    }
                                });
                            } else {
                                $('#local-games').text('No games found');
                            }
                        }
                    });
                });
                return false;
            });

            $('#create-geolocated-game').show().click(function () {
                getGeohash(function (geohash) {
                    $('#geohash').val(geohash);
                    document.getElementById('form').submit();
                });
                return false;
            });

            $('#create-game').show().click(function () {
                $('#geohash').val('');

                return true;
            });
        }

        // Show recent game results
        $.ajax({
            url: 'results',
            dataType: 'json',
            success: function (data) {
                $('#recent-results').html('');
                if (data.length) {
                    data.forEach(function (result) {
                        var el = $('
' +
                            playerName(result.winner) + (result.draw ? ' drew against ' : ' beat ') +
                            playerName(result.loser) + '
');
                        var board = $('

');
                        el.appendTo($('#recent-results'));
                        board.appendTo(el);
                        drawBoard(board[0], result.columns, result.rows, result.board);
                        if (!result.draw) {
                            highlightWinningCells(result.winning, board);
                        }
                    });
                } else {
                    $('#recent-results').text('No recent results');
                }
            }
        });
    }
});
<|endoftext|>


<commit_before>'use strict';

window.lively = this;

import * as scripts from './script-manager.js';
import * as messaging from './messaging.js';
import * as preferences from './preferences.js';
import * as persistence from './persistence.js';

import files from './files.js';
import html from './html.js';
import paths from './paths.js';

import inspector from './inspector.js';

import keys from './keys.js';
import components from './morphic/component-loader.js';

/* expose external modules */
import color from '../external/tinycolor.js';
import focalStorage from '../external/focalStorage.js';
import * as jquery from '../external/jquery.js';
import * as _ from '../external/underscore.js';

let $ = window.$,
  babel = window.babel,
  System = window.System; // known global variables.

// a) Special shorthands for interactive development
// b) this is the only reasonable way to use modules in template scripts, due to no shared lexical scope #TODO
var exportmodules = [
  "scripts",
  "messaging",
  "preferences",
  "persistence",
  "files",
  "keys",
  "paths",
  "html",
  "components",
  "inspector",
  "color",
  "focalStorage"];

// #LiveProgramming #Syntax #ES6Modules #Experiment #Jens
// By structuring our modules differently, we still can act as es6 module to the outside but develop at runtime
// #IDEA: I refactored from "static module and function style" to "dynamic object" style
export default class Lively {

  static import(moduleName, path, forceLoad) {
    if (!path) path = this.defaultPath(moduleName)
    if (!path) throw Error("Could not imoport " + moduleName + ", not path specified!")

    if (this[moduleName] && !forceLoad)
      return new Promise((resolve) => { resolve(this[moduleName])})
    if (forceLoad) {
      path += "?" + Date.now()
    }
    return System.import(path).then( (module, err) => {
      if (err) {
        lively.notify("Could not load module " + moduleName, err);
      } else {
        console.log("lively: load "+ moduleName)

        if (moduleName == "lively") {
          this.notify("migrate lively.js")
          var oldLively = window.lively;
          window.lively =module.default || module
          this["previous"] = oldLively
          this.components = oldLively.components // components have important state
        } else {
          this[moduleName] = module.default || module
        }

        if (lively.components && this[moduleName])
          lively.components.updatePrototype(this[moduleName].prototype);

        return module.default || module
      }
    })
  }

  static reloadModule(path, optSource) {
    if (!path) path = this.defaultPath(moduleName)
    if (!path) throw Error("Could not imoport " + moduleName + ", not path specified!")

    if (this[moduleName])
      return new Promise((resolve) => { resolve(this[moduleName])})
    return System.import(path).then( module => {
      console.log("lively: load "+ moduleName)
      this[moduleName] = module.default || module
    })
  }

  static defaultPath(moduleName) {
    return ({
      math: lively4url + "/src/external/math.js",
      typo: lively4url + "/src/external/typo.js",
      contextmenu: lively4url + '/src/client/contextmenu.js',
      customize: lively4url + '/src/client/customize.js'
    })[moduleName]
  }

  static loaded() {
    // #Refactor with #ContextJS
    // guard againsst wrapping twice and ending in endless recursion
    if (!console.log.isWrapped) {
        var nativeLog = console.log;
        console.log = function() {
            nativeLog.apply(console, arguments);
            lively.log.apply(undefined, arguments);
        };
        console.log.isWrapped = true;
        console.log.nativeLog = nativeLog; // #TODO use generic Wrapper mechanism here
    }
    if (!console.error.isWrapped) {
        var nativeLog = console.log;
        console.error = function() {
            nativeLog.apply(console, arguments);
            lively.log.apply(undefined, arguments);
        };
        console.error.isWrapped = true;
        console.error.nativeLog = nativeLog; // #TODO use generic Wrapper mechanism here
    }

    // General Error Handling
    if (window.onerror === null) {
      window.onerror  = function(message, source, lineno, colno, error) {
    	  window.LastError = error
    	  lively.notify("Error: ", message, 20, () =>
    		  lively.openWorkspace("Error:" + message + "\nLine:" + lineno + " Col: " + colno+"\nSource:" + source + "\nError:" + error.stack))
      }
    }

    exportmodules.forEach(name => lively[name] = eval(name)); // oh... this seems uglier than expected

    this.import("authGithub", lively4url + '/src/client/auth-github.js')
    this.import("authDropbox", lively4url + '/src/client/auth-dropbox.js')
  }

  static array(anyList){
    return Array.prototype.slice.call(anyList);
  }

  static openWorkspace(string, pos) {
    var name = "juicy-ace-editor";
    var comp  = document.createElement(name);
    return lively.components.openInWindow(comp).then((container) => {
      pos = pos || lively.pt(100,100);
      comp.changeMode("javascript");
      comp.enableAutocompletion();
      comp.editor.setValue(string)
      lively.setPosition(container,pos);
    }).then( () => {
      comp.editor.focus();
      return comp
    });
  }

  static boundEval(str, ctx) {
    var interactiveEval = function(text) { return eval(text) };
    var transpiledSource = babel.transform(str).code;
    // #TODO alt: babel.run
    // #TODO context does not seem to work!
    return interactiveEval.call(ctx, transpiledSource);
  }

  static pt(x,y) {
    return {x: x, y: y};
  }

  static setPosition(obj, point) {
      obj.style.position = "absolute";

      // var bounds = that.getBoundingClientRect().top
      //var deltax = point.x - bounds.left
      // var deltay = point.y - bounds.top

      // obj.style.left = ""+  ((obj.style.left || 0) - deltax) + "px";
      // obj.style.top = "" + ((obj.style.top || 0) - deltay) + "px";
      obj.style.left = ""+  point.x + "px";
      obj.style.top = "" +  point.y + "px";
  }

  static openFile(url) {
    if (url.hostname == "lively4"){
      var container  = $('lively-container')[0];
      if (container) {
        container.followPath(url.pathname);
      } else {
        console.log("fall back on editor: "+ url);
        this.editFile(url);
      }
    } else {
      this.editFile(url);
    }
  }

  static editFile(url) {
    var editor  = document.createElement("lively-editor");
    lively.components.openInWindow(editor).then((container) => {
        lively.setPosition(container, lively.pt(100, 100));
        editor.setURL(url);
        editor.loadFile();
    });
  }

  static hideContextMenu(evt) {
    // console.log("hide: " + (evt.path[0] === document.body))
    if (evt.path[0] !== document.body) return
    console.log("hide context menu:" + evt)
    this.import("contextmenu").then(m => m.hide());
  }

  static openContextMenu(container, evt, target) {
    if (HaloService.areHalosActive() ||
      (HaloService.halosHidden && ((Date.now() - HaloService.halosHidden) < 500))) {
      target = that
    }
    console.log("open context menu: " + target);
    this.import("contextmenu").then(m => m.openIn(container, evt, target));

  }

  static log(/* varargs */) {
      var args = arguments;
      $('lively-console').each(function() {
        try{
          if (this.log) this.log.apply(this, args);
        }catch(e) {
          // ignore...
        }
      });
  }

  static notify(title, text, timeout, cb) {
    // just in case...
    if (Notification.permission !== "granted") Notification.requestPermission();

    console.log("NOTE: " + title  + " (" + text + ")");
    var notification = new Notification(title || "", {
      icon: 'https://www.lively-kernel.org/media/livelylogo-small.png',
      body: text || "",
    });
    if (cb) notification.onclick = cb
    if (timeout === undefined) timeout = 3
    setTimeout(() => notification.close(), timeout * 1000);
    // notification.onclick = cb
  }

  static initializeDocument(doc, loadedAsExtension) {
    console.log("Lively4 initializeDocument");
      doc.addEventListener('contextmenu', function(evt) {
          if (evt.ctrlKey) {
            evt.preventDefault();
            lively.openContextMenu($('body')[0], evt);
            return false;
          }
      }, false);

    if (loadedAsExtension) {
      this.import("customize").then(customize => {
          customize.customizePage()
      })
      lively.notify("Lively4 extension loaded!",
        "  CTRL+LeftClick  ... open halo\n" +
        "  CTRL+RightClick ... open menu");
    } else {
      // doc.addEventListener('contextmenu', function(evt) {
      //   evt.preventDefault();
      //   lively.openContextMenu($('body')[0], evt);
      //   return false;
      // }, false);
    }

    doc.addEventListener('click', function(evt){lively.hideContextMenu(evt)}, false);
    doc.addEventListener('keyup', function(evt){lively.keys.handle(evt)}, false);
  }

  static initializeHalos() {
    if ($('lively-halos').size() === 0) {
        $('<lively-halos>')
            .attr('data-lively4-donotpersist', 'all')
            .appendTo($('body'));
    }
    lively.components.loadUnresolved();
  }

   static unload() {
      lively.notify("unloading Lively is not supported yet! Please reload page....");
  }

  static updateTemplate(html) {
    var tagName = components.reloadComponent(html);
    if (!tagName) return;

    _.each($(tagName), function(oldInstance) {
      if (oldInstance.__ingoreUpdates) return;

      var owner = oldInstance.parentElement;
      var newInstance = document.createElement(tagName);

      owner.replaceChild(newInstance, oldInstance);
      _.each(oldInstance.childNodes, function(ea) {
        newInstance.appendChild(ea);
        console.log("append old child: " + ea);

      });
      _.each(oldInstance.attributes, function(ea) {
        console.log("set old attribute " + ea.name + " to: " + ea.value);
        newInstance.setAttribute(ea.name, ea.value);

      });
      _.each(_.keys(oldInstance), function(ea) {
        console.log("ignore properties: " + newInstance[ea] + " <-- " + oldInstance[ea]);
      });
    });
  }

  static showPoint(point) {
    var comp = document.createElement("div")
    comp.style['pointer-events'] = "none"
    comp.style.width = "5px"
    comp.style.height = "5px"
    comp.style.padding = "1px"
    comp.style.backgroundColor = "red"
    document.body.appendChild(comp)
    lively.setPosition(comp, point)
    comp.setAttribute("data-is-meta", "true")

    setTimeout( () => $(comp).remove(), 3000)
    // ea.getBoundingClientRect

  }

  static showSource(object, evt) {
    if (object instanceof HTMLElement) {
        var comp  = document.createElement("lively-container");
        lively.components.openInWindow(comp).then((container) => {
          comp.editFile(lively4url +"/templates/" + object.localName + ".html")
        })
    } else {
      lively.notify("Could not show source for: " + object)
    }
  }

  static showElement(elem, timeout) {
    var comp = document.createElement("div")
    var bounds = elem.getBoundingClientRect()
    var pos = lively.pt(
      bounds.left +  $(document).scrollLeft(),
      bounds.top +  $(document).scrollTop())

    comp.style.width = bounds.width +"px"
    comp.style.height = bounds.height +"px"
    comp.style['pointer-events'] = "none"
    // comp.style.height = "0px"
    comp.style["z-index"] = 1000;
    comp.style.border = "1px solid red";
    document.body.appendChild(comp)
    lively.setPosition(comp, pos);
    comp.setAttribute("data-is-meta", "true")

    comp.innerHTML = "
" +
        elem.tagName +": " + elem.id + "\n" +
        elem.getAttribute("class") +"\n"

      + "
"

    setTimeout( () => $(comp).remove(), timeout || 3000)
    return comp
  }

  static allProperties(obj, result) {
    result = result || {};
    Object.getOwnPropertyNames(obj).forEach( name => {result[name] = obj.constructor.name})
    if (obj.__proto__) {
      lively.allProperties(obj.__proto__, result);
    }
    return result
  }

  static registerTemplate() {
    var template = document.currentScript.ownerDocument.querySelector('template');
    var clone = document.importNode(template.content, true);
    System.import('../src/client/morphic/component-loader.js').then(loader => { loader.register(template.id, clone); });
  }
}

window.lively = Lively
Lively.loaded();

console.log("loaded lively");
<commit_msg>SYNC  M src/client/lively.js;<commit_after>'use strict';

window.lively = this;

import * as scripts from './script-manager.js';
import * as messaging from './messaging.js';
import * as preferences from './preferences.js';
import * as persistence from './persistence.js';

import files from './files.js';
import html from './html.js';
import paths from './paths.js';

import inspector from './inspector.js';

import keys from './keys.js';
import components from './morphic/component-loader.js';

/* expose external modules */
import color from '../external/tinycolor.js';
import focalStorage from '../external/focalStorage.js';
import * as jquery from '../external/jquery.js';
import * as _ from '../external/underscore.js';

let $ = window.$,
  babel = window.babel,
  System = window.System; // known global variables.

// a) Special shorthands for interactive development
// b) this is the only reasonable way to use modules in template scripts, due to no shared lexical scope #TODO
var exportmodules = [
  "scripts",
  "messaging",
  "preferences",
  "persistence",
  "files",
  "keys",
  "paths",
  "html",
  "components",
  "inspector",
  "color",
  "focalStorage"];

// #LiveProgramming #Syntax #ES6Modules #Experiment #Jens
// By structuring our modules differently, we still can act as es6 module to the outside but develop at runtime
// #IDEA: I refactored from "static module and function style" to "dynamic object" style
export default class Lively {

  static import(moduleName, path, forceLoad) {
    if (!path) path = this.defaultPath(moduleName)
    if (!path) throw Error("Could not imoport " + moduleName + ", not path specified!")

    if (this[moduleName] && !forceLoad)
      return new Promise((resolve) => { resolve(this[moduleName])})
    if (forceLoad) {
      path += "?" + Date.now()
    }
    return System.import(path).then( (module, err) => {
      if (err) {
        lively.notify("Could not load module " + moduleName, err);
      } else {
        console.log("lively: load "+ moduleName)

        if (moduleName == "lively") {
          this.notify("migrate lively.js")
          var oldLively = window.lively;
          window.lively =module.default || module
          this["previous"] = oldLively
          this.components = oldLively.components // components have important state
        } else {
          this[moduleName] = module.default || module
        }

        if (lively.components && this[moduleName])
          lively.components.updatePrototype(this[moduleName].prototype);

        return module.default || module
      }
    })
  }

  static reloadModule(path, optSource) {
    if (!path) path = this.defaultPath(moduleName)
    if (!path) throw Error("Could not imoport " + moduleName + ", not path specified!")

    if (this[moduleName])
      return new Promise((resolve) => { resolve(this[moduleName])})
    return System.import(path).then( module => {
      console.log("lively: load "+ moduleName)
      this[moduleName] = module.default || module
    })
  }

  static defaultPath(moduleName) {
    return ({
      math: lively4url + "/src/external/math.js",
      typo: lively4url + "/src/external/typo.js",
      contextmenu: lively4url + '/src/client/contextmenu.js',
      customize: lively4url + '/src/client/customize.js'
    })[moduleName]
  }

  static loaded() {
    // #Refactor with #ContextJS
    // guard againsst wrapping twice and ending in endless recursion
    if (!console.log.originalFunction) {
        var nativeLog = console.log;
        console.log = function() {
            nativeLog.apply(console, arguments);
            lively.log.apply(undefined, arguments);
        };
        console.log.originalFunction = nativeLog; // #TODO use generic Wrapper mechanism here
    }
    if (!console.error.originalFunction) {
        var nativeError = console.error;
        console.error = function() {
            nativeError.apply(console, arguments);
            lively.log.apply(undefined, arguments);
        };
        console.error.originalFunction = nativeError; // #TODO use generic Wrapper mechanism here
    }

    // General Error Handling
    if (window.onerror === null) {
      window.onerror  = function(message, source, lineno, colno, error) {
    	  window.LastError = error
    	  lively.notify("Error: ", message, 20, () =>
    		  lively.openWorkspace("Error:" + message + "\nLine:" + lineno + " Col: " + colno+"\nSource:" + source + "\nError:" + error.stack))
      }
    }

    exportmodules.forEach(name => lively[name] = eval(name)); // oh... this seems uglier than expected

    this.import("authGithub", lively4url + '/src/client/auth-github.js')
    this.import("authDropbox", lively4url + '/src/client/auth-dropbox.js')
  }

  static array(anyList){
    return Array.prototype.slice.call(anyList);
  }

  static openWorkspace(string, pos) {
    var name = "juicy-ace-editor";
    var comp  = document.createElement(name);
    return lively.components.openInWindow(comp).then((container) => {
      pos = pos || lively.pt(100,100);
      comp.changeMode("javascript");
      comp.enableAutocompletion();
      comp.editor.setValue(string)
      lively.setPosition(container,pos);
    }).then( () => {
      comp.editor.focus();
      return comp
    });
  }

  static boundEval(str, ctx) {
    var interactiveEval = function(text) { return eval(text) };
    var transpiledSource = babel.transform(str).code;
    // #TODO alt: babel.run
    // #TODO context does not seem to work!
    return interactiveEval.call(ctx, transpiledSource);
  }

  static pt(x,y) {
    return {x: x, y: y};
  }

  static setPosition(obj, point) {
      obj.style.position = "absolute";

      // var bounds = that.getBoundingClientRect().top
      //var deltax = point.x - bounds.left
      // var deltay = point.y - bounds.top

      // obj.style.left = ""+  ((obj.style.left || 0) - deltax) + "px";
      // obj.style.top = "" + ((obj.style.top || 0) - deltay) + "px";
      obj.style.left = ""+  point.x + "px";
      obj.style.top = "" +  point.y + "px";
  }

  static openFile(url) {
    if (url.hostname == "lively4"){
      var container  = $('lively-container')[0];
      if (container) {
        container.followPath(url.pathname);
      } else {
        console.log("fall back on editor: "+ url);
        this.editFile(url);
      }
    } else {
      this.editFile(url);
    }
  }

  static editFile(url) {
    var editor  = document.createElement("lively-editor");
    lively.components.openInWindow(editor).then((container) => {
        lively.setPosition(container, lively.pt(100, 100));
        editor.setURL(url);
        editor.loadFile();
    });
  }

  static hideContextMenu(evt) {
    // console.log("hide: " + (evt.path[0] === document.body))
    if (evt.path[0] !== document.body) return
    console.log("hide context menu:" + evt)
    this.import("contextmenu").then(m => m.hide());
  }

  static openContextMenu(container, evt, target) {
    if (HaloService.areHalosActive() ||
      (HaloService.halosHidden && ((Date.now() - HaloService.halosHidden) < 500))) {
      target = that
    }
    console.log("open context menu: " + target);
    this.import("contextmenu").then(m => m.openIn(container, evt, target));

  }

  static log(/* varargs */) {
      var args = arguments;
      $('lively-console').each(function() {
        try{
          if (this.log) this.log.apply(this, args);
        }catch(e) {
          // ignore...
        }
      });
  }

  static notify(title, text, timeout, cb) {
    // just in case...
    if (Notification.permission !== "granted") Notification.requestPermission();

    console.log("NOTE: " + title  + " (" + text + ")");
    var notification = new Notification(title || "", {
      icon: 'https://www.lively-kernel.org/media/livelylogo-small.png',
      body: text || "",
    });
    if (cb) notification.onclick = cb
    if (timeout === undefined) timeout = 3
    setTimeout(() => notification.close(), timeout * 1000);
    // notification.onclick = cb
  }

  static initializeDocument(doc, loadedAsExtension) {
    console.log("Lively4 initializeDocument");
      doc.addEventListener('contextmenu', function(evt) {
          if (evt.ctrlKey) {
            evt.preventDefault();
            lively.openContextMenu($('body')[0], evt);
            return false;
          }
      }, false);

    if (loadedAsExtension) {
      this.import("customize").then(customize => {
          customize.customizePage()
      })
      lively.notify("Lively4 extension loaded!",
        "  CTRL+LeftClick  ... open halo\n" +
        "  CTRL+RightClick ... open menu");
    } else {
      // doc.addEventListener('contextmenu', function(evt) {
      //   evt.preventDefault();
      //   lively.openContextMenu($('body')[0], evt);
      //   return false;
      // }, false);
    }

    doc.addEventListener('click', function(evt){lively.hideContextMenu(evt)}, false);
    doc.addEventListener('keyup', function(evt){lively.keys.handle(evt)}, false);
  }

  static initializeHalos() {
    if ($('lively-halos').size() === 0) {
        $('<lively-halos>')
            .attr('data-lively4-donotpersist', 'all')
            .appendTo($('body'));
    }
    lively.components.loadUnresolved();
  }

   static unload() {
      lively.notify("unloading Lively is not supported yet! Please reload page....");
  }

  static updateTemplate(html) {
    var tagName = components.reloadComponent(html);
    if (!tagName) return;

    _.each($(tagName), function(oldInstance) {
      if (oldInstance.__ingoreUpdates) return;

      var owner = oldInstance.parentElement;
      var newInstance = document.createElement(tagName);

      owner.replaceChild(newInstance, oldInstance);
      _.each(oldInstance.childNodes, function(ea) {
        newInstance.appendChild(ea);
        console.log("append old child: " + ea);

      });
      _.each(oldInstance.attributes, function(ea) {
        console.log("set old attribute " + ea.name + " to: " + ea.value);
        newInstance.setAttribute(ea.name, ea.value);

      });
      _.each(_.keys(oldInstance), function(ea) {
        console.log("ignore properties: " + newInstance[ea] + " <-- " + oldInstance[ea]);
      });
    });
  }

  static showPoint(point) {
    var comp = document.createElement("div")
    comp.style['pointer-events'] = "none"
    comp.style.width = "5px"
    comp.style.height = "5px"
    comp.style.padding = "1px"
    comp.style.backgroundColor = "red"
    document.body.appendChild(comp)
    lively.setPosition(comp, point)
    comp.setAttribute("data-is-meta", "true")

    setTimeout( () => $(comp).remove(), 3000)
    // ea.getBoundingClientRect

  }

  static showSource(object, evt) {
    if (object instanceof HTMLElement) {
        var comp  = document.createElement("lively-container");
        lively.components.openInWindow(comp).then((container) => {
          comp.editFile(lively4url +"/templates/" + object.localName + ".html")
        })
    } else {
      lively.notify("Could not show source for: " + object)
    }
  }

  static showElement(elem, timeout) {
    var comp = document.createElement("div")
    var bounds = elem.getBoundingClientRect()
    var pos = lively.pt(
      bounds.left +  $(document).scrollLeft(),
      bounds.top +  $(document).scrollTop())

    comp.style.width = bounds.width +"px"
    comp.style.height = bounds.height +"px"
    comp.style['pointer-events'] = "none"
    // comp.style.height = "0px"
    comp.style["z-index"] = 1000;
    comp.style.border = "1px solid red";
    document.body.appendChild(comp)
    lively.setPosition(comp, pos);
    comp.setAttribute("data-is-meta", "true")

    comp.innerHTML = "
" +
        elem.tagName +": " + elem.id + "\n" +
        elem.getAttribute("class") +"\n"

      + "
"

    setTimeout( () => $(comp).remove(), timeout || 3000)
    return comp
  }

  static allProperties(obj, result) {
    result = result || {};
    Object.getOwnPropertyNames(obj).forEach( name => {result[name] = obj.constructor.name})
    if (obj.__proto__) {
      lively.allProperties(obj.__proto__, result);
    }
    return result
  }

  static registerTemplate() {
    var template = document.currentScript.ownerDocument.querySelector('template');
    var clone = document.importNode(template.content, true);
    System.import('../src/client/morphic/component-loader.js').then(loader => { loader.register(template.id, clone); });
  }
}

window.lively = Lively
Lively.loaded();

console.log("loaded lively");
<|endoftext|>


<commit_before>// hasOwnProperty is only used on browser-provided objects
/* eslint-disable no-prototype-builtins */
import './compatibility'
import translate from './translate'
import { defaultSettings, dismissalStates } from './defaults'

//
// Options
//

const options = [{
	name: 'borderType',
	kind: 'radio'
}, {
	name: 'borderColour',
	kind: 'individual',
	element: document.getElementById('border-colour'),
}, {
	name: 'borderFontSize',
	kind: 'individual',
	element: document.getElementById('border-font-size'),
}]

function restoreOptions() {
	browser.storage.sync.get(defaultSettings, function(items) {
		for (const option of options) {
			const name = option.name
			const saved = items[name]

			switch (option.kind) {
				case 'radio':
					document.getElementById(`radio-${saved}`).checked = true
					break
				case 'individual':
					option.element.value = saved
					break
				default:
					console.error(`Unexpected option kind '${option.kind}'`)
			}
		}
	})
}

function setUpOptionHandlers() {
	for (const option of options) {
		if (option.kind === 'individual') {
			option.element.addEventListener('change', () => {
				browser.storage.sync.set({
					[option.name]: option.element.value
				})
			})
		}
	}

	for (const radio of document.querySelectorAll('input[type="radio"]')) {
		radio.addEventListener('change', function() {
			const pref = this.parentElement.parentElement
				.getAttribute('data-pref')
			browser.storage.sync.set({
				[pref]: this.value
			})
		})
	}

	if (BROWSER === 'firefox' || BROWSER === 'opera') {
		document.getElementById('reset-messages').onclick = resetMessages
	}

	document.getElementById('reset-to-defaults').onclick = resetToDefaults
}

function updateResetDismissedMessagesButtonState() {
	const button = document.getElementById('reset-messages')
	const feedback = document.getElementById('reset-messages-feedback')

	browser.storage.sync.get(dismissalStates, function(items) {
		for (const dismissalState in items) {
			if (items[dismissalState] === true) {
				button.setAttribute('aria-disabled', false)
				feedback.innerText = null
				return
			}
		}

		button.setAttribute('aria-disabled', true)
		if (!feedback.innerText) {
			feedback.innerText =
				browser.i18n.getMessage('prefsResetMessagesNone')
		}
	})
}

function resetMessages() {
	if (this.getAttribute('aria-disabled') === String(false)) {
		browser.storage.sync.set(dismissalStates)  // default values are false
		document.getElementById('reset-messages-feedback')
			.innerText = browser.i18n.getMessage('prefsResetMessagesDone')
	}
}

function dismissalStateChanged(thingChanged) {
	return dismissalStates.hasOwnProperty(thingChanged)
}

function resetToDefaults() {
	browser.storage.sync.set(defaultSettings, function() {
		window.location.reload()
	})
	// Note: Can't use use .clear() as that removes everything, which would
	//       cause problems for currently-visible borders.
	// FIXME use restoreOptions instead?
	// FIXME resetting to defaults after seetting to sidebar still brings up the warning message (even though it's going back to toolbar) -- this is because it reloads and persists the selection across reloads
}

//
// Entryway
//

function main() {
	if (BROWSER === 'firefox' || BROWSER === 'opera') {
		options.push({
			name: 'interface',
			kind: 'radio'
		})

		updateResetDismissedMessagesButtonState()

		browser.storage.onChanged.addListener(function(changes) {
			if (Object.keys(changes).some(dismissalStateChanged)) {
				updateResetDismissedMessagesButtonState()
			}
		})
	}

	translate()
	restoreOptions()
	setUpOptionHandlers()
}

main()
<commit_msg>fix(Options): do not use aria-disabled for button (#357)<commit_after>// hasOwnProperty is only used on browser-provided objects
/* eslint-disable no-prototype-builtins */
import './compatibility'
import translate from './translate'
import { defaultSettings, dismissalStates } from './defaults'

//
// Options
//

const options = [{
	name: 'borderType',
	kind: 'radio'
}, {
	name: 'borderColour',
	kind: 'individual',
	element: document.getElementById('border-colour'),
}, {
	name: 'borderFontSize',
	kind: 'individual',
	element: document.getElementById('border-font-size'),
}]

function restoreOptions() {
	browser.storage.sync.get(defaultSettings, function(items) {
		for (const option of options) {
			const name = option.name
			const saved = items[name]

			switch (option.kind) {
				case 'radio':
					document.getElementById(`radio-${saved}`).checked = true
					break
				case 'individual':
					option.element.value = saved
					break
				default:
					console.error(`Unexpected option kind '${option.kind}'`)
			}
		}
	})
}

function setUpOptionHandlers() {
	for (const option of options) {
		if (option.kind === 'individual') {
			option.element.addEventListener('change', () => {
				browser.storage.sync.set({
					[option.name]: option.element.value
				})
			})
		}
	}

	for (const radio of document.querySelectorAll('input[type="radio"]')) {
		radio.addEventListener('change', function() {
			const pref = this.parentElement.parentElement
				.getAttribute('data-pref')
			browser.storage.sync.set({
				[pref]: this.value
			})
		})
	}

	if (BROWSER === 'firefox' || BROWSER === 'opera') {
		document.getElementById('reset-messages').onclick = resetMessages
	}

	document.getElementById('reset-to-defaults').onclick = resetToDefaults
}

function updateResetDismissedMessagesButtonState() {
	const button = document.getElementById('reset-messages')
	const feedback = document.getElementById('reset-messages-feedback')

	browser.storage.sync.get(dismissalStates, function(items) {
		for (const dismissalState in items) {
			if (items[dismissalState] === true) {
				button.dataset.someMessagesDismissed = true
				feedback.innerText = null
				return
			}
		}

		button.dataset.someMessagesDismissed = false
		if (!feedback.innerText) {
			feedback.innerText =
				browser.i18n.getMessage('prefsResetMessagesNone')
		}
	})
}

function resetMessages() {
	if (this.dataset.someMessagesDismissed === String(true)) {
		browser.storage.sync.set(dismissalStates)  // default values are false
		document.getElementById('reset-messages-feedback')
			.innerText = browser.i18n.getMessage('prefsResetMessagesDone')
	}
}

function dismissalStateChanged(thingChanged) {
	return dismissalStates.hasOwnProperty(thingChanged)
}

function resetToDefaults() {
	browser.storage.sync.set(defaultSettings, function() {
		window.location.reload()
	})
	// Note: Can't use use .clear() as that removes everything, which would
	//       cause problems for currently-visible borders.
	// FIXME use restoreOptions instead?
	// FIXME resetting to defaults after seetting to sidebar still brings up the warning message (even though it's going back to toolbar) -- this is because it reloads and persists the selection across reloads
}

//
// Entryway
//

function main() {
	if (BROWSER === 'firefox' || BROWSER === 'opera') {
		options.push({
			name: 'interface',
			kind: 'radio'
		})

		updateResetDismissedMessagesButtonState()

		browser.storage.onChanged.addListener(function(changes) {
			if (Object.keys(changes).some(dismissalStateChanged)) {
				updateResetDismissedMessagesButtonState()
			}
		})
	}

	translate()
	restoreOptions()
	setUpOptionHandlers()
}

main()
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2017-present PlatformIO <contact@platformio.org>
 * All rights reserved.
 *
 * This source code is licensed under the license found in the LICENSE file in
 * the root directory of this source tree.
 */

import { runPIOCommand } from '../utils';
import vscode from 'vscode';

export default async function initCommand() {
  if (!vscode.workspace.rootPath) {
    vscode.window.showWarningMessage(
      'PlatformIO project could not be initialized. Please open a folder '
      + 'first before performing initialization.'
    );
  }
  await vscode.window.withProgress({
    title: 'PlatformIO Project initialization...',
    location: vscode.ProgressLocation.Window,
  }, async (progress) => {
    progress.report({
      message: 'Updating a list of avaialbe boards',
    });

    try {
      const data = JSON.parse(await new Promise((resolve, reject) => {
        runPIOCommand(['boards', '--json-output'], (code, stdout, stderr) => {
          if (code !== 0) {
            reject(stderr);
          } else {
            resolve(stdout);
          }
        });
      }));
      const items = data.map((board) => ({
        label: board.name,
        description: board.vendor,
        detail: board.mcu,
        boardId: board.id,
      }));

      progress.report({
        message: 'Selecting a board',
      });
      const selectedBoard = await vscode.window.showQuickPick(items, {
        ignoreFocusOut: true,
        matchOnDescription: true,
        matchOnDetail: true,
        placeHolder: 'Select a board',
      });

      if (selectedBoard) {
        progress.report({
          message: 'Performing initialization',
        });

        await new Promise((resolve, reject) => {
          runPIOCommand(['init', '--ide', 'vscode', '--board', selectedBoard.boardId, '--project-dir', vscode.workspace.rootPath], (code, stdout, stderr) => {
            if (code !== 0) {
              reject(stderr);
            } else {
              resolve(stdout);
            }
          });
        });
      }
    } catch (error) {
      console.error(error);
      vscode.window.showErrorMessage(error);
    }
  });
}
<commit_msg>Initialize project without workspace // Resolve #11<commit_after>/**
 * Copyright (c) 2017-present PlatformIO <contact@platformio.org>
 * All rights reserved.
 *
 * This source code is licensed under the license found in the LICENSE file in
 * the root directory of this source tree.
 */

import { runPIOCommand } from '../utils';
import vscode from 'vscode';

export default async function initCommand() {
  if (!vscode.workspace.rootPath) {
    vscode.window.showErrorMessage(
      'Please open a folder (File > Open...) where PlatformIO should initialize a project, then repeat this command again.'
    );
    return;
  }
  await vscode.window.withProgress({
    title: 'PlatformIO Project initialization...',
    location: vscode.ProgressLocation.Window,
  }, async (progress) => {
    progress.report({
      message: 'Updating a list of avaialbe boards',
    });

    try {
      const data = JSON.parse(await new Promise((resolve, reject) => {
        runPIOCommand(['boards', '--json-output'], (code, stdout, stderr) => {
          if (code !== 0) {
            reject(stderr);
          } else {
            resolve(stdout);
          }
        });
      }));
      const items = data.map((board) => ({
        label: board.name,
        description: board.vendor,
        detail: board.mcu,
        boardId: board.id,
      }));

      progress.report({
        message: 'Selecting a board',
      });
      const selectedBoard = await vscode.window.showQuickPick(items, {
        ignoreFocusOut: true,
        matchOnDescription: true,
        matchOnDetail: true,
        placeHolder: 'Select a board',
      });

      if (selectedBoard) {
        progress.report({
          message: 'Performing initialization',
        });

        await new Promise((resolve, reject) => {
          runPIOCommand(['init', '--ide', 'vscode', '--board', selectedBoard.boardId, '--project-dir', vscode.workspace.rootPath], (code, stdout, stderr) => {
            if (code !== 0) {
              reject(stderr);
            } else {
              resolve(stdout);
            }
          });
        });
      }
    } catch (error) {
      console.error(error);
      vscode.window.showErrorMessage(error);
    }
  });
}
<|endoftext|>