Commit 07de98ed authored by Jon Parsons's avatar Jon Parsons Committed by Andrew Newdigate

Merge branch 'develop' into feature/topics-buttons

parents 5b3ec0b6 acbe8387
......@@ -9,21 +9,18 @@ All rights reserved.
Please symlink pre-commit to .git/hooks/pre-commit to enable the pre-commit hooks.
Prerequisites
-------------
* node.js 0.10+ `brew install node`
* [Virtualbox](https://www.virtualbox.org/wiki/Downloads)
* [Kitematic](https://kitematic.com/) or `boot2docker` if you prefer
Getting Started
---------------
2. Open Kitematic and choose "Install Docker Commands" from the application menu.
3. In the root directory of the project run `./start` to start your services
4. `npm install`
5. `npm install -g gulp`
6. `gulp css` (compiles css)
7. `npm install -g nodemon`
8. `nodemon` (this will run node and restart when anything changes based on config in nodemon.json)
1. install prerequisites:
* kitematic, not docker-for-mac (it is incompatible with our start script)
* node js
* gulp (via npm)
2. run `./start` to download and start all the docker images for our databases
3. run `npm install`
4. run `npm run link` (you will get weird errors if you dont)
5. run `gulp`
6. run `node web` and go to localhost:5000
Data Upgrades
-------------
......
# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure(2) do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://atlas.hashicorp.com/search.
config.vm.box = "ubuntu/trusty64"
config.vm.box_url = "https://oss-binaries.phusionpassenger.com/vagrant/boxes/latest/ubuntu-14.04-amd64-vbox.box"
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
config.vm.network "forwarded_port", guest: 5000, host: 5000
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
config.vm.network "public_network"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
config.vm.provider "virtualbox" do |vb|
# Display the VirtualBox GUI when booting the machine
vb.gui = true
# Customize the amount of memory on the VM:
vb.memory = 2048
vb.cpus = 2
end
#
# View the documentation for the provider you are using for more
# information on available options.
# Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
# such as FTP and Heroku are also available. See the documentation at
# https://docs.vagrantup.com/v2/push/atlas.html for more information.
# config.push.define "atlas" do |push|
# push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
# end
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
# config.vm.provision "shell", inline: <<-SHELL
# sudo apt-get update
# sudo apt-get install -y apache2
# SHELL
config.vm.provision "shell", path: "vagrant_provision.sh"
end
......@@ -66,8 +66,10 @@ mongosetup:
entrypoint: [ "/src/scripts/docker/mongo/mongo-setup.sh" ]
neo4j:
image: kbastani/docker-neo4j
image: neo4j:2.3.0
restart: always
environment:
NEO4J_AUTH: none
elasticsearch:
image: elasticsearch:1.4.2
......
......@@ -75,10 +75,12 @@ mongosetup:
entrypoint: [ "/src/scripts/docker/mongo/mongo-setup.sh" ]
neo4j:
image: kbastani/docker-neo4j
image: neo4j:2.3
restart: always
ports:
- "7474:7474"
environment:
NEO4J_AUTH: none
# The official elasticsearch:1.4.2 image with mapper-attachments and river-mongodb plugins
# https://github.com/soldotno/elasticsearch-river-mongodb/blob/master/Dockerfile
......
......@@ -20,6 +20,7 @@ function createMessage(fixtureName, f) {
meta: f.meta,
sent: f.sent,
editedAt: f.editedAt,
pub: f.pub || false,
readBy: f.readBy,
});
}
......
......@@ -12,7 +12,7 @@ export const ForumCategoryStore = Collection.extend({
model: CategoryModel,
initialize: function() {
this.listenTo(router, 'change:category', this.onCategoryUpdate, this);
this.listenTo(router, 'change:categoryName', this.onCategoryUpdate, this);
},
getCategories: function() {
......
@import "../../colors.less";
@import "../../text.less";
@import (less, reference) "~gitter-styleguide/css/components/buttons.css";
@import "../../../../../../public/less/markdown.less";
@import "../../colors.less";
@import "../../text.less";
.feed-item {
display: flex;
flex-direction: column;
}
//VARIABLES
//----------------------------
.feed-item__user-details {
width: 5rem;
padding-top: .5rem;
}
//Feed Item
@feed-item-vertical-spacing: 2rem;//The space provided at the bottom of each feed-item
.feed-item__content {
display: flex;
margin-bottom: 1.5rem;
}
//Aside
@feed-item-aside-width: 5rem; //Width of the left most column
@feed-item-aside-padding-top: .5rem; //Spaing between the avatar and the top of the feed-item__content
.feed-item__body {
position: relative;
width: 100%;
padding: 2rem;
background-color: @topic-reply-body-color;
//Content
@feed-item-content-background: @light; //Background color of the actual content item
@feed-item-content-padding: 2.5rem; //Padding of the content item
@feed-item-content-item-vertical-padding: 1rem; //Padding of user generated content
//MIXINS
//----------------------------
.m-details-text( @capitalize: true, @font-size: 1.2rem ) {
font-size: @font-size;
font-family: @font-family;
overflow: hidden;
word-wrap: break-word;
color: @topic-reply-action-text-color;
.markdown-styles();
& when (@capitalize = true) {
text-transform: uppercase;
}
}
.feed-item__footer {
display: flex;
align-items: center;
padding-top: 1rem;
//COMPONENTS
//----------------------------
//Skeychy but will be refactored when we refactor
//the feed-item/reply-list-item/comment-item components
button {
margin-right: .5rem;
}
//Feed item container
.feed-item {
display: flex;
margin-bottom: @feed-item-vertical-spacing;
}
.m-details-text() {
font-size: 1.2rem;
text-transform: uppercase;
font-family: @font-family;
color: @topic-reply-action-text-color;
//Left hand aside (should just display an avatar)
.feed-item__aside {
flex: 0 0 @feed-item-aside-width;
padding-top: @feed-item-aside-padding-top;
}
// TODO: remove
.feed-item__likes {
.m-details-text();
margin-right: 1rem;
//Main content holder
.feed-item__content {
width: 100%;
padding: @feed-item-content-padding;
background-color: @feed-item-content-background;
}
.feed-item__content__item {
.markdown-styles();
padding: @feed-item-content-item-vertical-padding 0;
font-family: @font-family;
font-size: @reply-content-font-size;
.feed-item__comments {
.m-details-text();
outline: none;
background: none;
border: none;
cursor: pointer;
//We can't guarantee what we get back as markdown
//paragraphs comeback with margin which we need to cancel as we provide
//padding for normal un-wrapped text elements
& p:first-child { margin-top: 0; }
& p:last-child { margin-bottom: 0; }
}
@content-control-top-margin: .5rem;
//Main header displays :
//[displayname | username, edit-button | sent date]
.feed-item__content__header { display: flex; }
.feed-item__sent {
.feed-item__content__header__user-display-name {
.m-details-text();
font-size: 1.1rem;
float: right;
margin-top: @content-control-top-margin;
margin-right: .5rem;
}
.feed-item__edit-control {
.m-details-text();
margin-top: @content-control-top-margin;
border: none;
background: none;
outline: none;
color: @dark;
cursor: pointer;
float: right;
.feed-item__content__header__user-user-name {
.m-details-text(false);
margin-right: auto;
}
.feed-item__content__header__sent { .m-details-text(); }
.feed-item__content__header__edit-button { margin-left: auto; }
//Item footer, usually contains:
//[ :+1:, comments, watch ]
.feed-item__content__footer { display: flex; }
......@@ -5,7 +5,7 @@
.topic-body__content {
padding: 3rem 0;
font-size: 1.6rem;
font-size: @topic-content-font-size;
font-family: @font-family;
.markdown-styles();
......@@ -31,6 +31,7 @@
cursor: pointer;
}
@content-control-top-margin: .5rem;
.topic-body__footer__action--edit {
margin-top: @content-control-top-margin;
float: right;
......
@font-family: 'Source Sans Pro', source-sans-pro, pt sans, calibri, sans-serif;
@topic-content-font-size: 1.6rem;
@reply-content-font-size: (@topic-content-font-size - .1rem);
......@@ -37,7 +37,7 @@ export default React.createClass({
</aside>
<Editor
autoFocus={autoFocus}
placeholder="Your reply here. Use Markdown, BBCode, or HTML to format."
placeholder="Your comment here. Use Markdown, BBCode, or HTML to format."
className="reply-comment-editor__editor"
value={value}
onEnter={this.onEnter}
......
......@@ -40,6 +40,7 @@ export default React.createClass({
const {item, children, footerChildren} = this.props;
const {user} = item;
const { displayName, username } = user;
const formattedSentDate = item.sent && moment(item.sent).format('MMM Do');
const formattedFullSentDate = item.sent && moment(item.sent).format('YYYY-MM-D, h:m A');
......@@ -47,34 +48,61 @@ export default React.createClass({
//TODO we need to make edit controls, reaction and follow buttons
//smart components so they can dispatch events. This will prevent us
//having to have these controls in scope to change state
return (
<article
id={item.id}
className="feed-item">
<div className="feed-item__content">
<div className="feed-item__user-details">
<div> { /* We have to use a wrapper div here to account for any children passed to this component*/ }
<article id={item.id} className="feed-item">
{/* The aside here only displays the avatar pulled to the left hand side*/}
<aside className="feed-item__aside">
<UserAvatar
className="feed-item__avatar"
user={user}
size={AVATAR_SIZE_MEDIUM} />
</div>
<div className="feed-item__body">
<span
className="feed-item__sent"
title={formattedFullSentDate}>
{formattedSentDate}
</span>
{this.getEditControl()}
{this.getItemContent()}
<footer className="feed-item__footer">
</aside>
<section className="feed-item__content">
{/* The header displays displayname | username edit-button | sent date */}
<header className="feed-item__content__header">
{/* Shows the users display name ie: Jon Parsons */}
<span className="feed-item__content__header__user-display-name">
{displayName}
</span>
{/* Shows the users username name ie: cutandpastey */}
<span className="feed-item__content__header__user-user-name">
@{username}
</span>
{/* Edit controls are only shown for admins or owners */}
{this.getEditControl()}
{/* sent date displayed as -> MM dd */}
<span className="feed-item__content__header__sent" title={formattedFullSentDate}>
{formattedSentDate}
</span>
</header>
{/* The actula user generated content */}
<div className="feed-item__content__item">
{this.getItemContent()}
</div>
{/* This will show like/comment/watch buttons */}
<footer className="feed-item__content__footer">
{footerChildren}
</footer>
</section>
</article>
</div>
</div>
{/* Typically the comments list is passed in as children */}
{children}
</article>
</div>
);
},
getEditControl(){
......@@ -87,7 +115,7 @@ export default React.createClass({
return (
<IconButton
className="feed-item__edit-control"
className="feed-item__content__header__edit-button"
type={ICONS_EDIT}
onClick={this.onEditClicked} />
);
......
......@@ -22,14 +22,14 @@ describe('ForumCategoryStore', function(){
});
it('should update the active element when the route changes', function(){
mockRouter.set('category', 'test-1');
mockRouter.set('categoryName', 'test-1');
assert.equal(categoryStore.at(0).get('active'), false);
assert(categoryStore.at(1).get('active'));
});
it('should dispatch un active:update event when the active category changes', function(){
categoryStore.on(UPDATE_ACTIVE_CATEGORY, handle);
mockRouter.set('category', 'test-2');
mockRouter.set('categoryName', 'test-2');
assert.equal(handle.callCount, 1);
});
......
......@@ -40,11 +40,12 @@ var config = {
{
test: /\.jsx?$/,
loader: 'babel',
exclude: /node_modules/,
exclude: [ /node_modules/ ],
query: {
presets: [
"es2015",
"react"
// https://github.com/babel/babel-loader/issues/149
require.resolve("babel-preset-es2015"),
require.resolve("babel-preset-react")
]
}
}
......@@ -60,7 +61,10 @@ var config = {
// Fix https://github.com/webpack/webpack/issues/1083#issuecomment-187627979
// Also see https://github.com/babel/babel-loader/issues/149
resolveLoader: {
root: path.join(__dirname, 'node_modules')
// ../../ so that it uses the node_modules parallel to the modules folder
// rather than the one in modules/topics-ui. Otherwise you have to run npm
// install in the topics-ui modules too.
root: path.join(__dirname, '../../node_modules')
},
plugins: [
new ExtractTextPlugin("style.css", { allChunks: false })
......
......@@ -94,6 +94,9 @@ function updateCommentsTotal(topicId, replyId) {
$max: {
lastChanged: now,
lastModified: now,
},
$inc: {
_tv: 1
}
};
......@@ -104,6 +107,9 @@ function updateCommentsTotal(topicId, replyId) {
},
$set: {
commentsTotal: commentsTotal
},
$inc: {
_tv: 1
}
};
......@@ -220,6 +226,9 @@ function updateCommentFields(topicId, replyId, commentId, fields) {
$set: fields,
$max: {
lastModified: lastModified
},
$inc: {
_tv: 1
}
};
return Comment.findOneAndUpdate(query, update, { new: true })
......
......@@ -116,7 +116,10 @@ function updateCategoryFields(categoryId, fields) {
_id: categoryId
};
var update = {
$set: fields
$set: fields,
$inc: {
_tv: 1
}
};
return ForumCategory.findOneAndUpdate(query, update, { new: true })
.lean()
......
......@@ -112,6 +112,9 @@ function updateRepliesTotal(topicId) {
},
$set: {
repliesTotal: repliesTotal
},
$inc: {
_tv: 1
}
};
......@@ -136,12 +139,10 @@ function updateRepliesTotal(topicId) {
return;
}
// if this update won, then patch the live collection with the latest
// lastChanged values and also the new total replies.
liveCollections.topics.emit('patch', topic.forumId, topicId, {
lastChanged: nowString,
repliesTotal: topic.repliesTotal
})
// Do an update rather than a patch so that lastChanged, repliesTotal AND
// replyingUsers will go out. replyingUsers requires serializers which
// aren't available from here anyway.
liveCollections.topics.emit('update', topic);
})
.then(function() {
// return the topic that got updated (if it was updated).
......@@ -208,6 +209,9 @@ function updateReplyFields(topicId, replyId, fields) {
$set: fields,
$max: {
lastModified: lastModified
},
$inc: {
_tv: 1
}
};
return Reply.findOneAndUpdate(query, update, { new: true })
......
......@@ -243,6 +243,9 @@ function updateTopicFields(topicId, fields) {
$max: {
// certainly modified, but not necessarily changed or edited.
lastModified: new Date()
},
$inc: {
_tv: 1
}
};
return Topic.findOneAndUpdate(query, update, { new: true })
......
......@@ -36,6 +36,11 @@ var CURRENT_META_DATA_VERSION = markdownMajorVersion;
/* @const */
var MAX_CHAT_EDIT_AGE_SECONDS = 600;
/**
* Milliseconds considered 'recent'
*/
var RECENT_WINDOW_MILLISECONDS = 60 * 60 * 1000; // 1 hour
var ObjectID = require('mongodb').ObjectID;
......@@ -217,15 +222,27 @@ exports.newChatMessageToTroupe = function(troupe, user, data) {
// Returns some recent public chats
exports.getRecentPublicChats = function() {
var twentyFourHoursAgo = new Date(Date.now() - 86400000);
var minRecentTime = Date.now() - RECENT_WINDOW_MILLISECONDS;
var minId = mongoUtils.createIdForTimestamp(minRecentTime);
return ChatMessage
.where({ pub: true })
.where({ sent: { $gt: twentyFourHoursAgo} })
.sort({ _id: -1 })
.limit(100)
.exec();
var aggregation = [{
$match: {
_id: { $gt: minId },
pub: true
}
}, {
$sample: {
size: 100
}
}, {
$sort: {
_id: -1
}
}];
return ChatMessage.aggregate(aggregation)
.read(mongoReadPrefs.secondaryPreferred)
.exec();
};
/**
......
'use strict';
process.env.DISABLE_API_LISTEN = '1';
var Promise = require('bluebird');
var fixtureLoader = require('gitter-web-test-utils/lib/test-fixtures');
var assert = require('assert');
describe('room-api', function() {
var app, request;
before(function() {
request = require("supertest-as-promised")(Promise);
app = require('../../server/api');
});
var fixture = fixtureLoader.setup({
user1: {
accessToken: 'web-internal'
},
user2: {
accessToken: 'web-internal'
},
troupe1: {
security: 'PUBLIC',
users: ['user1']
},
message1: {
user: 'user1',
troupe: 'troupe1',
text: 'HELLO',
sent: new Date(),
pub: 1
},
});
fixtureLoader.disableMongoTableScans();
it('GET /private/sample-chats', function() {
return request(app)
.get('/private/sample-chats')
.set('x-access-token', fixture.user1.accessToken)
.expect(200)
.then(function(result) {
var sampleChats = result.body;
assert(sampleChats.length >= 1);
var sampleChat = sampleChats[0];
assert(sampleChat.avatarUrl);
assert(sampleChat.username);
assert(sampleChat.displayName);
assert(sampleChat.room);
});
});
})
......@@ -23,7 +23,8 @@ describe('chatService', function() {
user: 'user1',
troupe: 'troupe1',
text: 'old_message',
sent: new Date("01/01/2014")
sent: new Date("01/01/2014"),
pub: true
},
message2: {
user: 'user1',
......@@ -168,4 +169,14 @@ describe('chatService', function() {
});
it('getRecentPublicChats #slow', function() {
fixtureLoader.disableMongoTableScans();
return chatService.getRecentPublicChats()
.then(function(chats) {
assert(chats.length >= 1);
});
});
});
......@@ -101,10 +101,10 @@ describe('topics-live-collection #slow', function() {
.then(checkEvent);
});
it('should emit a patch event when adding a reply', function() {
it('should emit an update event when adding a reply', function() {
var checkEvent = appEvents.addListener('dataChange2', {
url: '/forums/' + fixture.forum1.id + '/topics',
operation: 'patch',
operation: 'update',
type: 'topic',
model: {
id: fixture.topic1.id.toString(),
......@@ -123,7 +123,7 @@ describe('topics-live-collection #slow', function() {
return topicService.findById(fixture.topic1._id)
.then(function(topic) {
// lastChanged must now match the one we got in the patch event.
// lastChanged must now match the one we got in the event.
assert.ok(topic.lastChanged);
var lastChanged = new Date(event.model.lastChanged);
assert.strictEqual(topic.lastChanged.getTime(), lastChanged.getTime());