chat working

parent 27828a05
......@@ -464,6 +464,22 @@ CREATE TABLE `working_session` (
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='This table stores working session information. Every working session is related to one instruction and one supervisor (and the instruction is related to one method which is related to one student)';
--
-- Table structure for table `message`
--
DROP TABLE IF EXISTS `message`;
CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`id_stu` int(11) NOT NULL,
`id_sup` int(11) NOT NULL,
`ts` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`content` varchar(4096) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `id_stu_ibfk_1` FOREIGN KEY (`id_stu`) REFERENCES `student` (`id`) ON DELETE CASCADE,
CONSTRAINT `id_sup_ibfk_1` FOREIGN KEY (`id_sup`) REFERENCES `supervisor` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='This table contains all messages on student chat';
--
-- Temporary view structure for view `v_stu_last_ws_time`
--
DROP TABLE IF EXISTS `v_stu_last_ws_time`;
......
......@@ -128,3 +128,15 @@ ADD CONSTRAINT `stu_sup_ibfk_2` FOREIGN KEY (`id_sup`) REFERENCES `supervisor` (
DROP TABLE office;
source triggers-enrolments-integrity-constraints.sql;
DROP TABLE IF EXISTS `message`;
CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`id_stu` int(11) NOT NULL,
`id_sup` int(11) NOT NULL,
`ts` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`content` varchar(4096) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `id_stu_ibfk_1` FOREIGN KEY (`id_stu`) REFERENCES `student` (`id`) ON DELETE CASCADE,
CONSTRAINT `id_sup_ibfk_1` FOREIGN KEY (`id_sup`) REFERENCES `supervisor` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='This table contains all messages on student chat';
const htmlEncode = require('js-htmlencode').htmlEncode;
module.exports = {
/**
* Retrieves all messages in the student's chat
*/
get: function(req, res) {
var params = req.allParams();
if (!params.id_stu)
return res.badRequest("Invalid request");
Message.find({id_stu: params.id_stu})
.populate('supervisor')
.then((msgs) => {
return res.ok(msgs);
})
.catch((err) => {
sails.log.error('Error getting messages for student ${params.id_stu}: ' + err);
return res.serverError("Error when retrieving messages");
});
},
/**
* Saves a new message in the student's chat
*/
send: function(req, res) {
var msg = req.param('msg');
if (!msg.id_sup || !msg.id_stu || !msg.content)
return res.badRequest("Invalid message");
Message.create({
'supervisor': msg.id_sup,
'student': msg.id_stu,
'content': msg.content,
'ts': new Date().toISOString()
})
.then((msg) => {
Supervisor.findOne({id: msg.supervisor})
.then((sup) => {
var obj = msg.toJSON();
obj.supervisor = sup.toJSON();
sails.sockets.broadcast('studentRoom' + obj.student, 'chatmessage', obj, req.socket);
return res.ok(obj);
})
.catch((err) => {
if (err) throw err;
});
})
.catch((err) => {
console.log("Error adding new message: " + err);
return res.serverError('Message not saved');
});
}
};
/* global sails */
/**
* Message.js
*
* @description :: TODO: Write a short summary of how this model works and what it represents here.
* @docs :: http://sailsjs.org/#!documentation/models
*/
module.exports = {
tableName: 'message',
migrate: 'safe',
schema: true,
autoPK: false,
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
id: {
type: 'integer',
autoIncrement: true,
primaryKey: true,
unique: true
},
ts: {
columnName: 'ts',
type: "datetime",
required: true
},
supervisor: {
columnName: 'id_sup',
type: 'integer',
required: true,
model: 'Supervisor'
},
student: {
columnName: 'id_stu',
type: 'integer',
required: true,
model: 'Student'
},
content: {
type: "string",
size: 4096
},
toJSON: function () {
var obj = this.toObject();
return obj;
}
}
};
......@@ -133,7 +133,7 @@ module.exports = {
if (!supervisor.name || supervisor.name.length == 0)
supervisor.name = sails.__({phrase: 'no_name', locale: supervisor.lang});
if (!supervisor.surname || supervisor.surname.length == 0)
supervisor.surname = sails.__({phrase: 'no_surname', locale: supervisor.lang});
supervisor.surname = ""; //sails.__({phrase: 'no_surname', locale: supervisor.lang});
delete supervisor.password;
return supervisor;
......
......@@ -31,7 +31,8 @@
"angular-recaptcha": "^4.0.1",
"ui-bootstrap": "~2.5.0",
"ngInfiniteScroll": "^1.3.4",
"angular-file-saver": "^1.1.3"
"angular-file-saver": "^1.1.3",
"angular-scroll-glue": "^2.2.0"
},
"resolutions": {
"angular": ">=1 <1.3.0",
......
......@@ -74,6 +74,7 @@
"change_password": "Change password",
"change_cat_picto": "Change category pictogram",
"change_picture": "Change picture",
"chat": "Chat",
"child": "Child",
"click": "Click",
"click_login": "Click to login",
......@@ -132,6 +133,7 @@
"email_invalid": "Invalid email",
"email_match": "The emails must match",
"email_type": "E-mail address",
"empty_chat": "There are no messages in this chat",
"enable_sound_for": "Enable sound for",
"enabled": "Enabled. Clic for disable",
"english": "English",
......@@ -495,6 +497,7 @@
"tutor_not_deleted": "Tutor couldn't be removed from the student",
"tutor_not_found": "There is no tutor account in Pictogram with this email.",
"tutors": "Tutors",
"type_your_message_here": "Type your message here",
"unlink": "Unlink",
"up": "Up",
"undefined": "Undefined",
......
......@@ -74,6 +74,7 @@
"change_password": "Cambiar contraseña",
"change_cat_picto": "Cambiar pictograma de la categoría",
"change_picture": "Cambiar fotografía",
"chat": "Chat",
"child": "Niño",
"click": "Clic",
"click_login": "Volver a inicio de sesión",
......@@ -131,6 +132,7 @@
"email_invalid": "El email no es válido",
"email_match": "Los emails deben coincidir",
"email_type": "Introduce tu email",
"empty_chat": "No se ha enviado ningún mensaje a este chat",
"enable_sound_for": "Habilitar sonido para",
"enabled": "Activado. Clic para desactivar",
"english": "Inglés",
......@@ -493,6 +495,7 @@
"tutor_not_deleted": "El tutor no se ha podido desvincular del alumno",
"tutor_not_found": "No hay ningún tutor en Pictogram con ese correo electrónico",
"tutors": "Tutores",
"type_your_message_here": "Escriba el mensaje aquí",
"undefined": "Sin definir",
"unlink": "Desvincular",
"unlimited": "Ilimitada",
......
......@@ -21,7 +21,8 @@ var dashboardApp = angular.module('dashboardApp', [
'ngMask',
'vcRecaptcha',
'infinite-scroll',
'ngFileSaver'
'ngFileSaver',
'luegg.directives'
]);
/* Main constants */
......@@ -162,6 +163,12 @@ dashboardApp.config(function ($stateProvider, $urlRouterProvider) {
templateUrl: 'modules/student/views/reports.html',
controller: 'StudentReportsCtrl',
})
.state('student_chat', {
url: '/student/:idStudent/chat',
parent: 'student',
templateUrl: 'modules/student/views/chat.html',
controller: 'StudentChatCtrl',
})
.state('student_setup', {
url: '/student/:idStudent/setup',
parent: 'student',
......
'use strict';
//-----------------------
// Student Instructions Controller
//-----------------------
dashboardApp.filter('fromNow', function () {
return function (date) {
return moment(date).fromNow();
};
});
dashboardControllers.controller('StudentChatCtrl', function StudentInstructionsCtrl(
$rootScope, $scope, $stateParams, $http, config, $window, $translate, $modal, ngToast, newconfirm) {
// For tab navigation (here too, if the user refresh the page...)
$scope.nav.tab = 'chat';
//
// Obtain chat messages
//
$http
.get(config.backend+'/stu/'+ $scope.studentData.id +'/messages')
.success(function(data, status, headers, config) {
// Add to list
$scope.messages = data;
})
.error(function(data, status, headers, config) {
console.log("Error from API: " + data);
});
//
// Method for sending new message
//
$scope.send = function() {
if (!$scope.content || $scope.content.length == 0)
return;
var msgData = {
'id_stu': $scope.studentData.id,
'id_sup': $rootScope.user.id,
'content': $scope.content
};
// Socket notify
io.socket.post('/stu/messages', {msg: msgData}, function (resData, jwRes) {
if (jwRes.statusCode == 200) { // Ok
$scope.messages.push(resData);
$scope.content = "";
$scope.$apply();
}
else
console.log("Error from API: " + resData);
});
};
//
// Web sockets notifications
//
// Add new listener to the event
io.socket.off('chatmessage');
io.socket.on('chatmessage', function (data) {
$scope.messages.push(data);
$scope.$apply();
});
});
<!-- chat tab-->
<div class="panel panel-default student_tab_panel">
<div class="chat-panel-body" scroll-glue>
<div ng-if="messages.length == 0">
<span class="label label-info" translate>empty_chat</label>
</div>
<ul class="chat">
<li ng-class="{'left': user.id != msg.supervisor.id, 'right': user.id == msg.supervisor.id, 'clearfix': true}" ng-repeat="msg in messages">
<span ng-class="{'chat-img': true, 'pull-left': user.id != msg.supervisor.id, 'pull-right': user.id == msg.supervisor.id}">
<img ng-src="{{ msg.supervisor.pic }}" alt="User Avatar" class="img-circle" ng-if="messages[$index - 1].supervisor.id != msg.supervisor.id"/>
</span>
<div class="chat-body clearfix">
<div class="header">
<strong class="primary-font" ng-class="{'pull-right': user.id == msg.supervisor.id}" ng-if="messages[$index - 1].supervisor.id != msg.supervisor.id">
{{ msg.supervisor.name }} <span ng-if>"msg.supervisor.surname.length > 0">{{ msg.supervisor.surname }}</span>
</strong>
<small class="text-muted" ng-class="{'pull-right': user.id != msg.supervisor.id}">
<span class="glyphicon glyphicon-time"></span> {{ msg.ts | fromNow }}
</small>
</div>
<p>
<pre>{{ msg.content }}</pre>
</p>
</div>
</li>
</ul>
</div>
<div class="panel-footer">
<div class="row">
<div class="col-sm-10">
<textarea name="message" rows="3" id="btn-input" ng-model="content" class="form-control input-sm" placeholder="{{ 'type_your_message_here' | translate }}">
</textarea>
</div>
<div class="col-sm-2">
<button class="btn btn-success" id="btn-chat" ng-click="send()" translate>send</button>
</div>
</div>
</div>
</div>
......@@ -63,6 +63,10 @@
<li role="presentation" ng-class="{'active' : nav.tab == 'reports'}" ng-if="studentData.license.isValid && !user.isTutor">
<a href="/app/#/student/{{studentData.id}}/reports" ng-click="nav.tab = 'reports'"><i class="fa fa-bar-chart" aria-hidden="true"></i> {{ 'reports' | translate }}</a>
</li>
<li role="presentation" ng-class="{'active' : nav.tab == 'chat'}">
<a href="/app/#/student/{{studentData.id}}/chat" ng-click="nav.tab = 'chat'"
><span class="glyphicon glyphicon-comment" aria-hidden="true"></span> {{ 'chat' | translate }}</a>
</li>
<li role="presentation" ng-class="{'active' : nav.tab == 'setup'}">
<a href="/app/#/student/{{studentData.id}}/setup" ng-click="nav.tab = 'setup'"
><span class="glyphicon glyphicon-cog" aria-hidden="true"></span> {{ 'setup' | translate }}</a>
......
......@@ -103,8 +103,14 @@
</span>
<a
class="btn btn-default btn-lg student-list-btn" role="button" href="/app/#/student/{{student.id}}/chat"
alt="{{ 'chat' | translate }}" popover="{{ 'chat' | translate }}" popover-trigger="mouseenter">
<span class="glyphicon glyphicon-comment"></span>
</a>
<a
class="btn btn-default btn-lg student-list-btn" role="button" href="/app/#/student/{{student.id}}/setup"
alt="{{ 'setup' | translate }}" popover="{{ 'setup' | translate}}" popover-trigger="mouseenter">
alt="{{ 'setup' | translate }}" popover="{{ 'setup' | translate }}" popover-trigger="mouseenter">
<span class="glyphicon glyphicon-cog" aria-hidden="true"></span>
</a>
......
......@@ -1254,3 +1254,77 @@ input.editable.grid-name {
.btn-grid-back{
margin-top:0.5em;
}
/* ----------------------------------
Chat related styles
*/
.chat
{
list-style: none;
margin: 0;
padding: 0;
}
.chat li
{
margin: 10px;
padding-bottom: 5px;
border-bottom: 1px dotted #B3A9A9;
}
.chat li.left .chat-body
{
margin-left: 70px;
}
.chat li.right .chat-body
{
margin-right: 70px;
}
.chat .img-circle
{
width: 60px;
}
.chat pre
{
border: 0;
background-color: transparent;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.chat li .chat-body p
{
margin: 0;
color: #777777;
}
.panel .slidedown .glyphicon, .chat .glyphicon
{
margin-right: 5px;
}
.chat-panel-body
{
overflow-y: scroll;
height: 400px;
}
::-webkit-scrollbar-track
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: #F5F5F5;
}
::-webkit-scrollbar
{
width: 12px;
background-color: #F5F5F5;
}
::-webkit-scrollbar-thumb
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555;
}
......@@ -172,5 +172,10 @@ module.exports.policies = {
TPVController: {
init: ['tokenAuth'],
notify: true,
},
MessageController: {
get: ['tokenAuth', 'isSupervisorOfStudent'],
send: true
}
};
......@@ -107,6 +107,9 @@ module.exports.routes = {
'POST /stu/subscribe': 'StudentController.subscribe',
'POST /stu/unsubscribe': 'StudentController.unsubscribe',
'GET /stu/:id_stu/messages': 'MessageController.get',
'POST /stu/messages': 'MessageController.send', // this is through websocket directly
// Websocket request for propagating actions add, delete, modify...
'POST /stu/vocabulary': 'StudentController.vocabulary',
......
......@@ -26,6 +26,7 @@ module.exports = function (grunt) {
'assets/app/bower_components/angular-chart.js/dist/angular-chart.min.js',
'assets/app/bower_components/ng-lodash/build/ng-lodash.js',
'assets/app/bower_components/ngInfiniteScroll/build/ng-infinite-scroll.min.js',
'assets/app/bower_components/angular-scroll-glue/src/scrollglue.js',
'assets/app/bower_components/ng-file-upload/ng-file-upload-shim.min.js',
'assets/app/bower_components/ng-file-upload/ng-file-upload.min.js',
'assets/app/bower_components/ngImgCrop/compile/minified/ng-img-crop.js',
......@@ -61,6 +62,7 @@ module.exports = function (grunt) {
'assets/scripts/modules/student/controllers/session.js',
'assets/scripts/modules/student/controllers/reports.js',
'assets/scripts/modules/student/controllers/instructions.js',
'assets/scripts/modules/student/controllers/chat.js',
'assets/scripts/modules/student/controllers/collections.js',
'assets/scripts/modules/student/controllers/addpicto.js',
'assets/scripts/modules/student/controllers/tags.js',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment