2014-08-13 04:32:18 +02:00
/ *
2014-09-03 18:29:13 +02:00
Copyright 2014 OpenMarket Ltd
2014-08-13 04:32:18 +02:00
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 .
* /
2014-09-04 11:19:28 +02:00
angular . module ( 'RoomController' , [ 'ngSanitize' , 'matrixFilter' , 'mFileInput' ] )
2014-09-18 19:12:21 +02:00
. controller ( 'RoomController' , [ '$filter' , '$scope' , '$timeout' , '$routeParams' , '$location' , '$rootScope' , 'matrixService' , 'mPresence' , 'eventHandlerService' , 'mFileUpload' , 'matrixPhoneService' , 'MatrixCall' ,
function ( $filter , $scope , $timeout , $routeParams , $location , $rootScope , matrixService , mPresence , eventHandlerService , mFileUpload , matrixPhoneService , MatrixCall ) {
2014-08-12 16:10:52 +02:00
'use strict' ;
2014-08-16 01:14:47 +02:00
var MESSAGES _PER _PAGINATION = 30 ;
2014-08-21 14:30:41 +02:00
var THUMBNAIL _SIZE = 320 ;
2014-08-18 17:11:08 +02:00
2014-08-18 17:40:05 +02:00
// Room ids. Computed and resolved in onInit
$scope . room _id = undefined ;
2014-08-18 17:11:08 +02:00
$scope . room _alias = undefined ;
2014-08-12 16:10:52 +02:00
$scope . state = {
user _id : matrixService . config ( ) . user _id ,
2014-09-11 16:54:51 +02:00
permission _denied : undefined , // If defined, this string contains the reason why the user cannot join the room
2014-08-21 16:27:15 +02:00
first _pagination : true , // this is toggled off when the first pagination is done
2014-09-09 16:46:30 +02:00
can _paginate : false , // this is toggled off when we are not ready yet to paginate or when we run out of items
2014-08-16 01:14:47 +02:00
paginating : false , // used to avoid concurrent pagination requests pulling in dup contents
2014-09-09 14:18:08 +02:00
stream _failure : undefined , // the response when the stream fails
2014-09-17 17:12:52 +02:00
waiting _for _joined _event : false , // true when the join request is pending. Back to false once the corresponding m.room.member event is received
2014-09-19 17:20:27 +02:00
messages _visibility : "hidden" , // In order to avoid flickering when scrolling down the message table at the page opening, delay the message table display
2014-08-12 16:10:52 +02:00
} ;
$scope . members = { } ;
2014-08-17 03:56:34 +02:00
$scope . autoCompleting = false ;
$scope . autoCompleteIndex = 0 ;
$scope . autoCompleteOriginal = "" ;
2014-08-13 11:42:28 +02:00
$scope . imageURLToSend = "" ;
2014-08-12 16:10:52 +02:00
$scope . userIDToInvite = "" ;
2014-08-14 18:23:47 +02:00
2014-09-12 10:48:06 +02:00
// vars and functions for updating the name
$scope . name = {
isEditing : false ,
newNameText : "" ,
editName : function ( ) {
if ( $scope . name . isEditing ) {
console . log ( "Warning: Already editing name." ) ;
return ;
} ;
2014-09-17 15:46:12 +02:00
var nameEvent = $rootScope . events . rooms [ $scope . room _id ] [ 'm.room.name' ] ;
if ( nameEvent ) {
$scope . name . newNameText = nameEvent . content . name ;
}
else {
$scope . name . newNameText = "" ;
}
2014-09-12 10:48:06 +02:00
// Force focus to the input
$timeout ( function ( ) {
angular . element ( '.roomNameInput' ) . focus ( ) ;
} , 0 ) ;
$scope . name . isEditing = true ;
} ,
updateName : function ( ) {
console . log ( "Updating name to " + $scope . name . newNameText ) ;
matrixService . setName ( $scope . room _id , $scope . name . newNameText ) . then (
function ( ) {
} ,
function ( error ) {
$scope . feedback = "Request failed: " + error . data . error ;
}
) ;
$scope . name . isEditing = false ;
} ,
cancelEdit : function ( ) {
$scope . name . isEditing = false ;
}
} ;
2014-09-09 03:40:34 +02:00
// vars and functions for updating the topic
$scope . topic = {
isEditing : false ,
newTopicText : "" ,
editTopic : function ( ) {
if ( $scope . topic . isEditing ) {
console . log ( "Warning: Already editing topic." ) ;
return ;
}
2014-09-09 03:59:26 +02:00
var topicEvent = $rootScope . events . rooms [ $scope . room _id ] [ 'm.room.topic' ] ;
if ( topicEvent ) {
$scope . topic . newTopicText = topicEvent . content . topic ;
}
else {
$scope . topic . newTopicText = "" ;
}
2014-09-11 11:49:59 +02:00
// Force focus to the input
$timeout ( function ( ) {
angular . element ( '.roomTopicInput' ) . focus ( ) ;
} , 0 ) ;
2014-09-09 03:40:34 +02:00
$scope . topic . isEditing = true ;
} ,
updateTopic : function ( ) {
console . log ( "Updating topic to " + $scope . topic . newTopicText ) ;
2014-09-11 11:53:37 +02:00
matrixService . setTopic ( $scope . room _id , $scope . topic . newTopicText ) . then (
function ( ) {
} ,
function ( error ) {
$scope . feedback = "Request failed: " + error . data . error ;
}
) ;
2014-09-09 03:40:34 +02:00
$scope . topic . isEditing = false ;
} ,
cancelEdit : function ( ) {
$scope . topic . isEditing = false ;
}
2014-09-12 10:48:06 +02:00
} ;
2014-09-09 03:40:34 +02:00
2014-09-05 17:52:11 +02:00
var scrollToBottom = function ( force ) {
2014-08-16 23:05:31 +02:00
console . log ( "Scrolling to bottom" ) ;
2014-09-05 17:52:11 +02:00
// Do not autoscroll to the bottom to display the new event if the user is not at the bottom.
// Exception: in case where the event is from the user, we want to force scroll to the bottom
var objDiv = document . getElementById ( "messageTableWrapper" ) ;
if ( ( objDiv . offsetHeight + objDiv . scrollTop >= objDiv . scrollHeight ) || force ) {
$timeout ( function ( ) {
objDiv . scrollTop = objDiv . scrollHeight ;
2014-09-17 17:12:52 +02:00
// Show the message table once the first scrolldown is done
if ( "visible" !== $scope . state . messages _visibility ) {
$timeout ( function ( ) {
$scope . state . messages _visibility = "visible" ;
} , 0 ) ;
}
2014-09-05 17:52:11 +02:00
} , 0 ) ;
}
2014-08-14 18:23:47 +02:00
} ;
2014-08-29 15:00:20 +02:00
2014-08-15 12:31:13 +02:00
$scope . $on ( eventHandlerService . MSG _EVENT , function ( ngEvent , event , isLive ) {
2014-08-15 13:51:20 +02:00
if ( isLive && event . room _id === $scope . room _id ) {
2014-09-05 17:52:11 +02:00
scrollToBottom ( ) ;
2014-08-14 18:23:47 +02:00
}
2014-08-15 12:31:13 +02:00
} ) ;
$scope . $on ( eventHandlerService . MEMBER _EVENT , function ( ngEvent , event , isLive ) {
2014-09-17 09:41:21 +02:00
if ( isLive && event . room _id === $scope . room _id ) {
2014-09-09 14:18:08 +02:00
if ( $scope . state . waiting _for _joined _event ) {
// The user has successfully joined the room, we can getting data for this room
$scope . state . waiting _for _joined _event = false ;
onInit3 ( ) ;
}
2014-09-11 16:54:51 +02:00
else if ( event . state _key === $scope . state . user _id && "invite" !== event . membership && "join" !== event . membership ) {
var user ;
if ( $scope . members [ event . user _id ] ) {
user = $scope . members [ event . user _id ] . displayname ;
}
if ( user ) {
user = user + " (" + event . user _id + ")" ;
}
else {
user = event . user _id ;
}
if ( "ban" === event . membership ) {
$scope . state . permission _denied = "You have been banned by " + user ;
}
else {
$scope . state . permission _denied = "You have been kicked by " + user ;
2014-09-17 09:41:21 +02:00
}
2014-09-11 16:54:51 +02:00
}
2014-09-09 14:18:08 +02:00
else {
scrollToBottom ( ) ;
updateMemberList ( event ) ;
2014-09-17 09:41:21 +02:00
// Notify when a user joins
if ( ( document . hidden || matrixService . presence . unavailable === mPresence . getState ( ) )
&& event . state _key !== $scope . state . user _id && "join" === event . membership ) {
var notification = new window . Notification (
event . content . displayname +
" (" + ( matrixService . getRoomIdToAliasMapping ( event . room _id ) || event . room _id ) + ")" , // FIXME: don't leak room_ids here
{
"body" : event . content . displayname + " joined" ,
"icon" : event . content . avatar _url ? event . content . avatar _url : undefined
} ) ;
$timeout ( function ( ) {
notification . close ( ) ;
} , 5 * 1000 ) ;
}
2014-09-09 14:18:08 +02:00
}
2014-09-02 09:39:43 +02:00
}
2014-08-15 12:31:13 +02:00
} ) ;
$scope . $on ( eventHandlerService . PRESENCE _EVENT , function ( ngEvent , event , isLive ) {
2014-09-02 09:39:43 +02:00
if ( isLive ) {
updatePresence ( event ) ;
}
2014-08-15 12:31:13 +02:00
} ) ;
2014-09-03 14:12:56 +02:00
$scope . $on ( eventHandlerService . POWERLEVEL _EVENT , function ( ngEvent , event , isLive ) {
if ( isLive && event . room _id === $scope . room _id ) {
for ( var user _id in event . content ) {
updateUserPowerLevel ( user _id ) ;
}
}
} ) ;
2014-08-27 19:57:54 +02:00
2014-08-29 15:00:20 +02:00
$scope . memberCount = function ( ) {
return Object . keys ( $scope . members ) . length ;
} ;
2014-08-14 18:23:47 +02:00
2014-08-15 18:42:02 +02:00
$scope . paginateMore = function ( ) {
if ( $scope . state . can _paginate ) {
2014-08-16 01:14:47 +02:00
// console.log("Paginating more.");
2014-08-16 23:05:31 +02:00
paginate ( MESSAGES _PER _PAGINATION ) ;
2014-08-15 18:42:02 +02:00
}
} ;
2014-08-28 20:03:34 +02:00
2014-08-16 23:05:31 +02:00
var paginate = function ( numItems ) {
2014-09-13 12:35:36 +02:00
//console.log("paginate " + numItems + " and first_pagination is " + $scope.state.first_pagination);
2014-08-20 17:08:05 +02:00
if ( $scope . state . paginating || ! $scope . room _id ) {
2014-08-16 01:14:47 +02:00
return ;
}
else {
$scope . state . paginating = true ;
}
2014-09-10 12:01:00 +02:00
console . log ( "paginateBackMessages from " + $rootScope . events . rooms [ $scope . room _id ] . pagination . earliest _token + " for " + numItems ) ;
2014-08-16 01:14:47 +02:00
var originalTopRow = $ ( "#messageTable>tbody>tr:first" ) [ 0 ] ;
2014-09-10 12:01:00 +02:00
// Paginate events from the point in cache
matrixService . paginateBackMessages ( $scope . room _id , $rootScope . events . rooms [ $scope . room _id ] . pagination . earliest _token , numItems ) . then (
2014-08-14 18:23:47 +02:00
function ( response ) {
2014-09-10 12:01:00 +02:00
2014-09-16 15:03:07 +02:00
eventHandlerService . handleRoomMessages ( $scope . room _id , response . data , false , 'b' ) ;
2014-08-14 18:23:47 +02:00
if ( response . data . chunk . length < MESSAGES _PER _PAGINATION ) {
2014-08-16 01:14:47 +02:00
// no more messages to paginate. this currently never gets turned true again, as we never
// expire paginated contents in the current implementation.
2014-08-14 18:23:47 +02:00
$scope . state . can _paginate = false ;
}
2014-08-15 18:42:02 +02:00
2014-08-16 23:05:31 +02:00
$scope . state . paginating = false ;
var wrapper = $ ( "#messageTableWrapper" ) [ 0 ] ;
var table = $ ( "#messageTable" ) [ 0 ] ;
// console.log("wrapper height=" + wrapper.clientHeight + ", table scrollHeight=" + table.scrollHeight);
if ( $scope . state . can _paginate ) {
// check we don't have to pull in more messages
// n.b. we dispatch through a timeout() to allow the digest to run otherwise the .height methods are stale
$timeout ( function ( ) {
if ( table . scrollHeight < wrapper . clientHeight ) {
paginate ( MESSAGES _PER _PAGINATION ) ;
scrollToBottom ( ) ;
}
} , 0 ) ;
}
2014-08-21 16:27:15 +02:00
if ( $scope . state . first _pagination ) {
2014-09-13 12:35:36 +02:00
scrollToBottom ( true ) ;
2014-08-21 16:27:15 +02:00
$scope . state . first _pagination = false ;
2014-08-15 18:42:02 +02:00
}
2014-08-16 01:14:47 +02:00
else {
2014-08-16 23:05:31 +02:00
// lock the scroll position
2014-08-16 01:14:47 +02:00
$timeout ( function ( ) {
// FIXME: this risks a flicker before the scrollTop is actually updated, but we have to
// dispatch it into a function in order to first update the layout. The right solution
// might be to implement it as a directive, more like
// http://stackoverflow.com/questions/23736647/how-to-retain-scroll-position-of-ng-repeat-in-angularjs
// however, this specific solution breaks because it measures the rows height before
// the contents are interpolated.
2014-08-16 23:05:31 +02:00
wrapper . scrollTop = originalTopRow ? ( originalTopRow . offsetTop + wrapper . scrollTop ) : 0 ;
2014-08-16 01:14:47 +02:00
} , 0 ) ;
}
2014-08-14 18:23:47 +02:00
} ,
function ( error ) {
2014-08-14 18:40:27 +02:00
console . log ( "Failed to paginateBackMessages: " + JSON . stringify ( error ) ) ;
2014-08-16 01:14:47 +02:00
$scope . state . paginating = false ;
2014-08-14 18:23:47 +02:00
}
2014-08-20 17:08:05 +02:00
) ;
2014-08-14 18:23:47 +02:00
} ;
2014-08-12 16:10:52 +02:00
var updateMemberList = function ( chunk ) {
2014-08-22 11:56:09 +02:00
if ( chunk . room _id != $scope . room _id ) return ;
2014-09-03 11:38:24 +02:00
2014-08-26 11:24:47 +02:00
// set target_user_id to keep things clear
var target _user _id = chunk . state _key ;
var isNewMember = ! ( target _user _id in $scope . members ) ;
2014-08-12 16:10:52 +02:00
if ( isNewMember ) {
2014-09-05 18:05:23 +02:00
// Ignore banned and kicked (leave) people
if ( "ban" === chunk . membership || "leave" === chunk . membership ) {
return ;
}
2014-08-16 14:22:18 +02:00
// FIXME: why are we copying these fields around inside chunk?
2014-09-01 19:09:17 +02:00
if ( "presence" in chunk . content ) {
chunk . presence = chunk . content . presence ;
2014-08-15 18:47:45 +02:00
}
2014-09-01 19:09:17 +02:00
if ( "last_active_ago" in chunk . content ) {
chunk . last _active _ago = chunk . content . last _active _ago ;
2014-09-02 16:39:17 +02:00
$scope . now = new Date ( ) . getTime ( ) ;
chunk . last _updated = $scope . now ;
2014-08-16 02:07:23 +02:00
}
2014-08-16 14:22:18 +02:00
if ( "displayname" in chunk . content ) {
chunk . displayname = chunk . content . displayname ;
}
if ( "avatar_url" in chunk . content ) {
chunk . avatar _url = chunk . content . avatar _url ;
}
2014-08-29 18:21:57 +02:00
$scope . members [ target _user _id ] = chunk ;
2014-08-22 11:50:38 +02:00
2014-08-26 11:24:47 +02:00
if ( target _user _id in $rootScope . presence ) {
updatePresence ( $rootScope . presence [ target _user _id ] ) ;
2014-08-22 11:50:38 +02:00
}
2014-08-12 16:10:52 +02:00
}
else {
2014-09-01 16:21:13 +02:00
// selectively update membership and presence else it will nuke the picture and displayname too :/
2014-09-05 18:05:23 +02:00
// Remove banned and kicked (leave) people
if ( "ban" === chunk . membership || "leave" === chunk . membership ) {
delete $scope . members [ target _user _id ] ;
return ;
}
2014-08-26 11:24:47 +02:00
var member = $scope . members [ target _user _id ] ;
2014-09-01 16:21:13 +02:00
member . membership = chunk . content . membership ;
2014-09-01 19:09:17 +02:00
if ( "presence" in chunk . content ) {
member . presence = chunk . content . presence ;
2014-09-01 16:21:13 +02:00
}
2014-09-01 19:09:17 +02:00
if ( "last_active_ago" in chunk . content ) {
member . last _active _ago = chunk . content . last _active _ago ;
2014-09-02 16:39:17 +02:00
$scope . now = new Date ( ) . getTime ( ) ;
member . last _updated = $scope . now ;
2014-09-01 16:21:13 +02:00
}
2014-08-12 16:10:52 +02:00
}
2014-08-29 13:30:20 +02:00
} ;
2014-08-29 18:21:57 +02:00
var updateMemberListPresenceAge = function ( ) {
$scope . now = new Date ( ) . getTime ( ) ;
2014-08-29 18:54:11 +02:00
// TODO: don't bother polling every 5s if we know none of our counters are younger than 1 minute
2014-08-29 18:21:57 +02:00
$timeout ( updateMemberListPresenceAge , 5 * 1000 ) ;
} ;
2014-08-12 16:10:52 +02:00
var updatePresence = function ( chunk ) {
if ( ! ( chunk . content . user _id in $scope . members ) ) {
console . log ( "updatePresence: Unknown member for chunk " + JSON . stringify ( chunk ) ) ;
return ;
}
var member = $scope . members [ chunk . content . user _id ] ;
2014-08-16 02:07:23 +02:00
// XXX: why not just pass the chunk straight through?
2014-09-01 19:09:17 +02:00
if ( "presence" in chunk . content ) {
member . presence = chunk . content . presence ;
2014-08-16 02:07:23 +02:00
}
2014-09-01 19:09:17 +02:00
if ( "last_active_ago" in chunk . content ) {
member . last _active _ago = chunk . content . last _active _ago ;
2014-09-02 16:39:17 +02:00
$scope . now = new Date ( ) . getTime ( ) ;
member . last _updated = $scope . now ;
2014-08-12 16:10:52 +02:00
}
// this may also contain a new display name or avatar url, so check.
if ( "displayname" in chunk . content ) {
member . displayname = chunk . content . displayname ;
}
if ( "avatar_url" in chunk . content ) {
member . avatar _url = chunk . content . avatar _url ;
}
2014-08-29 13:30:20 +02:00
} ;
2014-08-12 16:10:52 +02:00
2014-09-02 11:54:11 +02:00
var updateUserPowerLevel = function ( user _id ) {
var member = $scope . members [ user _id ] ;
if ( member ) {
member . powerLevel = matrixService . getUserPowerLevel ( $scope . room _id , user _id ) ;
2014-09-03 18:55:27 +02:00
normaliseMembersPowerLevels ( ) ;
}
2014-09-05 11:13:33 +02:00
} ;
2014-09-03 18:55:27 +02:00
// Normalise users power levels so that the user with the higher power level
// will have a bar covering 100% of the width of his avatar
var normaliseMembersPowerLevels = function ( ) {
// Find the max power level
var maxPowerLevel = 0 ;
for ( var i in $scope . members ) {
2014-09-24 12:22:40 +02:00
if ( ! $scope . members . hasOwnProperty ( i ) ) continue ;
2014-09-03 18:55:27 +02:00
var member = $scope . members [ i ] ;
if ( member . powerLevel ) {
maxPowerLevel = Math . max ( maxPowerLevel , member . powerLevel ) ;
}
}
// Normalized them on a 0..100% scale to be use in css width
if ( maxPowerLevel ) {
for ( var i in $scope . members ) {
2014-09-24 12:22:40 +02:00
if ( ! $scope . members . hasOwnProperty ( i ) ) continue ;
2014-09-03 18:55:27 +02:00
var member = $scope . members [ i ] ;
member . powerLevelNorm = ( member . powerLevel * 100 ) / maxPowerLevel ;
}
2014-09-02 11:54:11 +02:00
}
2014-09-05 11:13:33 +02:00
} ;
2014-09-02 11:54:11 +02:00
2014-08-12 16:10:52 +02:00
$scope . send = function ( ) {
2014-09-20 01:49:45 +02:00
var input = $ ( '#mainInput' ) . val ( ) ;
if ( undefined === input || input === "" ) {
2014-08-12 16:10:52 +02:00
return ;
}
2014-08-22 02:54:37 +02:00
2014-09-05 17:52:11 +02:00
scrollToBottom ( true ) ;
2014-09-17 14:18:39 +02:00
// Store the command in the history
2014-09-20 01:49:45 +02:00
history . push ( input ) ;
2014-09-17 14:18:39 +02:00
2014-08-12 16:10:52 +02:00
var promise ;
2014-09-06 19:13:38 +02:00
var cmd ;
var args ;
var echo = false ;
2014-09-03 11:07:44 +02:00
// Check for IRC style commands first
2014-09-04 18:40:15 +02:00
// trim any trailing whitespace, as it can confuse the parser for IRC-style commands
2014-09-20 01:49:45 +02:00
input = input . replace ( /\s+$/ , "" ) ;
2014-09-04 18:40:15 +02:00
2014-09-20 01:49:45 +02:00
if ( input [ 0 ] === "/" && input [ 1 ] !== "/" ) {
var bits = input . match ( /^(\S+?)( +(.*))?$/ ) ;
2014-09-06 19:13:38 +02:00
cmd = bits [ 1 ] ;
args = bits [ 3 ] ;
2014-09-04 18:40:15 +02:00
console . log ( "cmd: " + cmd + ", args: " + args ) ;
2014-09-03 11:07:44 +02:00
switch ( cmd ) {
case "/me" :
2014-09-04 18:40:15 +02:00
promise = matrixService . sendEmoteMessage ( $scope . room _id , args ) ;
2014-09-06 19:13:38 +02:00
echo = true ;
2014-09-03 11:07:44 +02:00
break ;
case "/nick" :
// Change user display name
2014-09-05 17:23:41 +02:00
if ( args ) {
promise = matrixService . setDisplayName ( args ) ;
}
else {
$scope . feedback = "Usage: /nick <display_name>" ;
}
2014-09-03 11:07:44 +02:00
break ;
2014-09-06 09:31:57 +02:00
case "/join" :
// Join a room
if ( args ) {
var matches = args . match ( /^(\S+)$/ ) ;
if ( matches ) {
var room _alias = matches [ 1 ] ;
if ( room _alias . indexOf ( ':' ) == - 1 ) {
// FIXME: actually track the :domain style name of our homeserver
// with or without port as is appropriate and append it at this point
}
var room _id = matrixService . getAliasToRoomIdMapping ( room _alias ) ;
console . log ( "joining " + room _alias + " id=" + room _id ) ;
if ( $rootScope . events . rooms [ room _id ] ) {
// don't send a join event for a room you're already in.
$location . url ( "room/" + room _alias ) ;
}
else {
promise = matrixService . joinAlias ( room _alias ) . then (
function ( response ) {
2014-09-23 19:50:39 +02:00
// TODO: factor out the common housekeeping whenever we try to join a room or alias
matrixService . roomState ( response . room _id ) . then (
function ( response ) {
2014-09-23 21:01:11 +02:00
eventHandlerService . handleEvents ( response . data , false , true ) ;
2014-09-23 19:50:39 +02:00
} ,
function ( error ) {
$scope . feedback = "Failed to get room state for: " + response . room _id ;
}
) ;
2014-09-06 09:31:57 +02:00
$location . url ( "room/" + room _alias ) ;
} ,
function ( error ) {
$scope . feedback = "Can't join room: " + JSON . stringify ( error . data ) ;
}
) ;
}
}
}
else {
$scope . feedback = "Usage: /join <room_alias>" ;
}
break ;
2014-09-03 11:17:58 +02:00
2014-09-03 16:59:09 +02:00
case "/kick" :
2014-09-05 17:23:41 +02:00
// Kick a user from the room with an optional reason
if ( args ) {
var matches = args . match ( /^(\S+?)( +(.*))?$/ ) ;
2014-09-05 17:30:50 +02:00
if ( matches ) {
promise = matrixService . kick ( $scope . room _id , matches [ 1 ] , matches [ 3 ] ) ;
2014-09-05 17:23:41 +02:00
}
2014-09-04 18:40:15 +02:00
}
2014-09-05 17:23:41 +02:00
if ( ! promise ) {
2014-09-04 18:40:15 +02:00
$scope . feedback = "Usage: /kick <userId> [<reason>]" ;
2014-09-03 16:59:09 +02:00
}
break ;
2014-09-03 11:17:58 +02:00
2014-09-04 18:40:15 +02:00
case "/ban" :
2014-09-05 17:23:41 +02:00
// Ban a user from the room with an optional reason
if ( args ) {
var matches = args . match ( /^(\S+?)( +(.*))?$/ ) ;
if ( matches ) {
promise = matrixService . ban ( $scope . room _id , matches [ 1 ] , matches [ 3 ] ) ;
}
2014-09-04 18:40:15 +02:00
}
2014-09-05 17:23:41 +02:00
if ( ! promise ) {
2014-09-04 18:40:15 +02:00
$scope . feedback = "Usage: /ban <userId> [<reason>]" ;
2014-09-03 11:17:58 +02:00
}
break ;
2014-09-04 18:40:15 +02:00
2014-09-03 15:58:28 +02:00
case "/unban" :
2014-09-03 16:59:09 +02:00
// Unban a user from the room
2014-09-05 17:23:41 +02:00
if ( args ) {
var matches = args . match ( /^(\S+)$/ ) ;
if ( matches ) {
// Reset the user membership to "leave" to unban him
2014-09-05 17:30:50 +02:00
promise = matrixService . unban ( $scope . room _id , matches [ 1 ] ) ;
2014-09-05 17:23:41 +02:00
}
2014-09-04 18:40:15 +02:00
}
2014-09-05 17:23:41 +02:00
if ( ! promise ) {
2014-09-04 18:40:15 +02:00
$scope . feedback = "Usage: /unban <userId>" ;
2014-09-03 15:58:28 +02:00
}
break ;
2014-09-03 14:12:56 +02:00
case "/op" :
2014-09-03 16:59:09 +02:00
// Define the power level of a user
2014-09-05 17:23:41 +02:00
if ( args ) {
var matches = args . match ( /^(\S+?)( +(\d+))?$/ ) ;
var powerLevel = 50 ; // default power level for op
if ( matches ) {
var user _id = matches [ 1 ] ;
2014-09-10 17:37:51 +02:00
if ( matches . length === 4 && undefined !== matches [ 3 ] ) {
2014-09-05 17:23:41 +02:00
powerLevel = parseInt ( matches [ 3 ] ) ;
}
if ( powerLevel !== NaN ) {
promise = matrixService . setUserPowerLevel ( $scope . room _id , user _id , powerLevel ) ;
}
2014-09-04 18:40:15 +02:00
}
}
2014-09-05 17:23:41 +02:00
2014-09-04 18:40:15 +02:00
if ( ! promise ) {
$scope . feedback = "Usage: /op <userId> [<power level>]" ;
2014-09-03 14:12:56 +02:00
}
break ;
2014-09-03 15:14:13 +02:00
case "/deop" :
2014-09-03 16:59:09 +02:00
// Reset the power level of a user
2014-09-05 17:23:41 +02:00
if ( args ) {
var matches = args . match ( /^(\S+)$/ ) ;
if ( matches ) {
promise = matrixService . setUserPowerLevel ( $scope . room _id , args , undefined ) ;
}
2014-09-04 18:40:15 +02:00
}
2014-09-05 17:23:41 +02:00
if ( ! promise ) {
2014-09-04 18:40:15 +02:00
$scope . feedback = "Usage: /deop <userId>" ;
2014-09-03 15:14:13 +02:00
}
break ;
2014-09-04 18:40:15 +02:00
default :
$scope . feedback = ( "Unrecognised IRC-style command: " + cmd ) ;
break ;
2014-09-03 11:07:44 +02:00
}
2014-08-29 18:24:13 +02:00
}
2014-09-03 15:25:59 +02:00
2014-09-04 18:40:15 +02:00
// By default send this as a message unless it's an IRC-style command
2014-09-06 19:13:38 +02:00
if ( ! promise && ! cmd ) {
// Make the request
2014-09-20 01:49:45 +02:00
promise = matrixService . sendTextMessage ( $scope . room _id , input ) ;
2014-09-06 19:13:38 +02:00
echo = true ;
}
if ( echo ) {
2014-09-05 14:09:14 +02:00
// Echo the message to the room
// To do so, create a minimalist fake text message event and add it to the in-memory list of room messages
var echoMessage = {
content : {
2014-09-20 01:49:45 +02:00
body : ( cmd === "/me" ? args : input ) ,
2014-09-06 19:13:38 +02:00
msgtype : ( cmd === "/me" ? "m.emote" : "m.text" ) ,
2014-09-05 14:09:14 +02:00
} ,
2014-10-18 00:11:55 +02:00
origin _server _ts : new Date ( ) . getTime ( ) , // fake a timestamp
2014-09-05 14:09:14 +02:00
room _id : $scope . room _id ,
type : "m.room.message" ,
user _id : $scope . state . user _id ,
2014-09-10 18:24:03 +02:00
echo _msg _state : "messagePending" // Add custom field to indicate the state of this fake message to HTML
2014-09-05 14:09:14 +02:00
} ;
2014-09-20 01:49:45 +02:00
$ ( '#mainInput' ) . val ( '' ) ;
2014-09-05 14:09:14 +02:00
$rootScope . events . rooms [ $scope . room _id ] . messages . push ( echoMessage ) ;
scrollToBottom ( ) ;
2014-08-12 16:10:52 +02:00
}
2014-09-04 18:40:15 +02:00
if ( promise ) {
2014-09-10 17:40:34 +02:00
// Reset previous feedback
$scope . feedback = "" ;
2014-09-04 18:40:15 +02:00
promise . then (
2014-09-10 18:24:03 +02:00
function ( response ) {
2014-09-04 18:40:15 +02:00
console . log ( "Request successfully sent" ) ;
2014-09-12 19:19:32 +02:00
2014-09-10 18:24:03 +02:00
if ( echo ) {
// Mark this fake message event with its allocated event_id
// When the true message event will come from the events stream (in handleMessage),
// we will be able to replace the fake one by the true one
echoMessage . event _id = response . data . event _id ;
2014-09-05 14:09:14 +02:00
}
else {
2014-09-20 01:49:45 +02:00
$ ( '#mainInput' ) . val ( '' ) ;
2014-09-10 18:24:03 +02:00
}
2014-09-04 18:40:15 +02:00
} ,
function ( error ) {
$scope . feedback = "Request failed: " + error . data . error ;
2014-09-05 14:09:14 +02:00
if ( echoMessage ) {
// Mark the message as unsent for the rest of the page life
2014-10-18 00:11:55 +02:00
echoMessage . origin _server _ts = "Unsent" ;
2014-09-05 14:09:14 +02:00
echoMessage . echo _msg _state = "messageUnSent" ;
}
2014-09-04 18:40:15 +02:00
} ) ;
}
2014-08-12 16:10:52 +02:00
} ;
$scope . onInit = function ( ) {
console . log ( "onInit" ) ;
2014-08-22 11:50:38 +02:00
2014-08-18 17:11:08 +02:00
// Does the room ID provided in the URL?
2014-08-18 17:40:05 +02:00
var room _id _or _alias ;
if ( $routeParams . room _id _or _alias ) {
room _id _or _alias = decodeURIComponent ( $routeParams . room _id _or _alias ) ;
}
if ( room _id _or _alias && '!' === room _id _or _alias [ 0 ] ) {
2014-08-28 16:23:20 +02:00
// Yes. We can go on right now
2014-08-18 17:40:05 +02:00
$scope . room _id = room _id _or _alias ;
2014-08-18 17:11:08 +02:00
$scope . room _alias = matrixService . getRoomIdToAliasMapping ( $scope . room _id ) ;
onInit2 ( ) ;
}
else {
2014-08-18 17:40:05 +02:00
// No. The URL contains the room alias. Get this alias.
if ( room _id _or _alias ) {
// The room alias was passed urlencoded, use it as is
$scope . room _alias = room _id _or _alias ;
2014-08-18 17:11:08 +02:00
}
2014-08-18 17:40:05 +02:00
else {
// Else get the room alias by hand from the URL
// ie: extract #public:localhost:8080 from http://127.0.0.1:8000/#/room/#public:localhost:8080
if ( 3 === location . hash . split ( "#" ) . length ) {
$scope . room _alias = "#" + location . hash . split ( "#" ) [ 2 ] ;
}
else {
// In case of issue, go to the default page
console . log ( "Error: cannot extract room alias" ) ;
2014-08-22 11:43:54 +02:00
$location . url ( "/" ) ;
2014-08-18 17:40:05 +02:00
return ;
}
2014-08-18 17:11:08 +02:00
}
// Need a room ID required in Matrix API requests
2014-08-18 17:40:05 +02:00
console . log ( "Resolving alias: " + $scope . room _alias ) ;
2014-08-18 17:11:08 +02:00
matrixService . resolveRoomAlias ( $scope . room _alias ) . then ( function ( response ) {
$scope . room _id = response . data . room _id ;
console . log ( " -> Room ID: " + $scope . room _id ) ;
2014-08-28 16:23:20 +02:00
// Now, we can go on
2014-08-18 17:11:08 +02:00
onInit2 ( ) ;
} ,
function ( ) {
// In case of issue, go to the default page
console . log ( "Error: cannot resolve room alias" ) ;
2014-08-22 11:43:54 +02:00
$location . url ( "/" ) ;
2014-08-18 17:11:08 +02:00
} ) ;
}
} ;
2014-08-28 16:23:20 +02:00
2014-08-18 17:11:08 +02:00
var onInit2 = function ( ) {
2014-08-28 16:23:20 +02:00
console . log ( "onInit2" ) ;
2014-09-16 16:13:24 +02:00
// Scroll down as soon as possible so that we point to the last message
// if it already exists in memory
scrollToBottom ( true ) ;
2014-08-28 16:23:20 +02:00
// Make sure the initialSync has been before going further
eventHandlerService . waitForInitialSyncCompletion ( ) . then (
2014-08-12 16:10:52 +02:00
function ( ) {
2014-09-01 16:27:11 +02:00
2014-08-28 16:23:20 +02:00
var needsToJoin = true ;
// The room members is available in the data fetched by initialSync
if ( $rootScope . events . rooms [ $scope . room _id ] ) {
2014-09-16 15:42:31 +02:00
2014-09-25 14:46:11 +02:00
var messages = $rootScope . events . rooms [ $scope . room _id ] . messages ;
if ( 0 === messages . length
|| ( 1 === messages . length && "m.room.member" === messages [ 0 ] . type && "invite" === messages [ 0 ] . content . membership && $scope . state . user _id === messages [ 0 ] . state _key ) ) {
// If we just joined a room, we won't have this history from initial sync, so we should try to paginate it anyway
$scope . state . first _pagination = true ;
2014-09-18 19:17:27 +02:00
}
else {
2014-09-25 14:46:11 +02:00
// There is no need to do a 1st pagination (initialSync provided enough to fill a page)
$scope . state . first _pagination = false ;
2014-09-18 19:17:27 +02:00
}
2014-09-16 15:42:31 +02:00
2014-08-28 16:23:20 +02:00
var members = $rootScope . events . rooms [ $scope . room _id ] . members ;
2014-08-12 16:10:52 +02:00
2014-08-28 16:23:20 +02:00
// Update the member list
for ( var i in members ) {
2014-09-24 12:22:40 +02:00
if ( ! members . hasOwnProperty ( i ) ) continue ;
2014-08-28 16:23:20 +02:00
var member = members [ i ] ;
updateMemberList ( member ) ;
}
// Check if the user has already join the room
if ( $scope . state . user _id in members ) {
if ( "join" === members [ $scope . state . user _id ] . membership ) {
needsToJoin = false ;
2014-08-12 16:10:52 +02:00
}
}
2014-08-28 16:23:20 +02:00
}
2014-08-14 18:23:47 +02:00
2014-08-28 16:23:20 +02:00
// Do we to join the room before starting?
if ( needsToJoin ) {
2014-09-09 14:18:08 +02:00
$scope . state . waiting _for _joined _event = true ;
2014-08-28 16:23:20 +02:00
matrixService . join ( $scope . room _id ) . then (
function ( ) {
2014-09-23 19:50:39 +02:00
// TODO: factor out the common housekeeping whenever we try to join a room or alias
matrixService . roomState ( $scope . room _id ) . then (
function ( response ) {
2014-09-23 21:01:11 +02:00
eventHandlerService . handleEvents ( response . data , false , true ) ;
2014-09-23 19:50:39 +02:00
} ,
function ( error ) {
console . error ( "Failed to get room state for: " + $scope . room _id ) ;
}
) ;
2014-09-09 14:18:08 +02:00
// onInit3 will be called once the joined m.room.member event is received from the events stream
// This avoids to get the joined information twice in parallel:
// - one from the events stream
// - one from the pagination because the pagination window covers this event ts
2014-08-28 16:23:20 +02:00
console . log ( "Joined room " + $scope . room _id ) ;
} ,
function ( reason ) {
2014-09-03 11:45:30 +02:00
console . log ( "Can't join room: " + JSON . stringify ( reason ) ) ;
2014-09-23 19:50:39 +02:00
// FIXME: what if it wasn't a perms problem?
2014-09-11 16:54:51 +02:00
$scope . state . permission _denied = "You do not have permission to join this room" ;
2014-08-28 16:23:20 +02:00
} ) ;
}
else {
onInit3 ( ) ;
}
}
) ;
} ;
var onInit3 = function ( ) {
console . log ( "onInit3" ) ;
// Make recents highlight the current room
$scope . recentsSelectedRoomID = $scope . room _id ;
2014-09-01 16:21:13 +02:00
2014-09-17 14:38:33 +02:00
// Init the history for this room
history . init ( ) ;
// Get the up-to-date the current member list
2014-09-01 16:21:13 +02:00
matrixService . getMemberList ( $scope . room _id ) . then (
function ( response ) {
for ( var i = 0 ; i < response . data . chunk . length ; i ++ ) {
var chunk = response . data . chunk [ i ] ;
updateMemberList ( chunk ) ;
2014-09-02 11:54:11 +02:00
// Add his power level
updateUserPowerLevel ( chunk . user _id ) ;
2014-09-01 16:21:13 +02:00
}
2014-09-02 11:14:45 +02:00
// Arm list timing update timer
updateMemberListPresenceAge ( ) ;
2014-09-09 16:46:30 +02:00
2014-09-16 15:42:31 +02:00
// Allow pagination
2014-09-09 16:46:30 +02:00
$scope . state . can _paginate = true ;
2014-09-16 15:42:31 +02:00
// Do a first pagination only if it is required
// FIXME: Should be no more require when initialSync/{room_id} will be available
if ( $scope . state . first _pagination ) {
paginate ( MESSAGES _PER _PAGINATION ) ;
}
else {
// There are already messages, go to the last message
scrollToBottom ( true ) ;
}
2014-09-01 16:21:13 +02:00
} ,
function ( error ) {
$scope . feedback = "Failed get member list: " + error . data . error ;
}
) ;
2014-08-12 16:10:52 +02:00
} ;
2014-09-11 13:52:07 +02:00
$scope . inviteUser = function ( ) {
2014-08-12 16:10:52 +02:00
2014-09-11 13:52:07 +02:00
matrixService . invite ( $scope . room _id , $scope . userIDToInvite ) . then (
2014-08-12 16:10:52 +02:00
function ( ) {
console . log ( "Invited." ) ;
2014-09-11 13:52:07 +02:00
$scope . feedback = "Invite successfully sent to " + $scope . userIDToInvite ;
$scope . userIDToInvite = "" ;
2014-08-12 16:10:52 +02:00
} ,
function ( reason ) {
$scope . feedback = "Failure: " + reason ;
} ) ;
} ;
$scope . leaveRoom = function ( ) {
matrixService . leave ( $scope . room _id ) . then (
function ( response ) {
2014-09-24 02:12:59 +02:00
console . log ( "Left room " + $scope . room _id ) ;
2014-08-22 18:08:03 +02:00
$location . url ( "home" ) ;
2014-08-12 16:10:52 +02:00
} ,
2014-08-14 16:47:38 +02:00
function ( error ) {
$scope . feedback = "Failed to leave room: " + error . data . error ;
2014-08-12 16:10:52 +02:00
} ) ;
} ;
2014-08-20 16:18:50 +02:00
$scope . sendImage = function ( url , body ) {
2014-09-05 17:52:11 +02:00
scrollToBottom ( true ) ;
2014-08-20 16:18:50 +02:00
matrixService . sendImageMessage ( $scope . room _id , url , body ) . then (
2014-08-13 11:42:28 +02:00
function ( ) {
console . log ( "Image sent" ) ;
} ,
2014-08-14 16:47:38 +02:00
function ( error ) {
$scope . feedback = "Failed to send image: " + error . data . error ;
2014-08-13 11:42:28 +02:00
} ) ;
} ;
2014-08-14 18:23:47 +02:00
2014-08-14 18:53:05 +02:00
$scope . imageFileToSend ;
$scope . $watch ( "imageFileToSend" , function ( newValue , oldValue ) {
if ( $scope . imageFileToSend ) {
2014-08-21 14:30:41 +02:00
// Upload this image with its thumbnail to Internet
mFileUpload . uploadImageAndThumbnail ( $scope . imageFileToSend , THUMBNAIL _SIZE ) . then (
function ( imageMessage ) {
// imageMessage is complete message structure, send it as is
matrixService . sendMessage ( $scope . room _id , undefined , imageMessage ) . then (
function ( ) {
console . log ( "Image message sent" ) ;
2014-08-20 16:18:50 +02:00
} ,
function ( error ) {
2014-08-21 14:30:41 +02:00
$scope . feedback = "Failed to send image message: " + error . data . error ;
} ) ;
2014-08-14 18:53:05 +02:00
} ,
function ( error ) {
2014-08-21 14:30:41 +02:00
$scope . feedback = "Can't upload image" ;
2014-08-20 16:18:50 +02:00
}
2014-08-14 18:53:05 +02:00
) ;
}
} ) ;
2014-08-14 18:23:47 +02:00
$scope . loadMoreHistory = function ( ) {
2014-08-16 23:05:31 +02:00
paginate ( MESSAGES _PER _PAGINATION ) ;
2014-08-14 18:23:47 +02:00
} ;
2014-08-27 19:57:54 +02:00
$scope . startVoiceCall = function ( ) {
var call = new MatrixCall ( $scope . room _id ) ;
2014-09-01 18:15:26 +02:00
call . onError = $rootScope . onCallError ;
call . onHangup = $rootScope . onCallHangup ;
2014-09-17 17:26:35 +02:00
// remote video element is used for playing audio in voice calls
call . remoteVideoElement = angular . element ( '#remoteVideo' ) [ 0 ] ;
call . placeVoiceCall ( ) ;
2014-09-09 18:37:50 +02:00
$rootScope . currentCall = call ;
} ;
$scope . startVideoCall = function ( ) {
var call = new MatrixCall ( $scope . room _id ) ;
call . onError = $rootScope . onCallError ;
call . onHangup = $rootScope . onCallHangup ;
2014-09-17 17:26:35 +02:00
call . localVideoElement = angular . element ( '#localVideo' ) [ 0 ] ;
call . remoteVideoElement = angular . element ( '#remoteVideo' ) [ 0 ] ;
call . placeVideoCall ( ) ;
2014-09-01 18:15:26 +02:00
$rootScope . currentCall = call ;
2014-09-05 11:13:33 +02:00
} ;
2014-08-28 20:03:34 +02:00
2014-09-17 14:18:39 +02:00
// Manage history of typed messages
2014-09-17 14:38:33 +02:00
// History is saved in sessionStoratge so that it survives when the user
// navigates through the rooms and when it refreshes the page
2014-09-17 14:18:39 +02:00
var history = {
// The list of typed messages. Index 0 is the more recents
data : [ ] ,
// The position in the history currently displayed
position : - 1 ,
// The message the user has started to type before going into the history
typingMessage : undefined ,
2014-09-17 14:38:33 +02:00
// Init/load data for the current room
init : function ( ) {
var data = sessionStorage . getItem ( "history_" + $scope . room _id ) ;
if ( data ) {
this . data = JSON . parse ( data ) ;
}
} ,
2014-09-17 14:18:39 +02:00
// Store a message in the history
push : function ( message ) {
this . data . unshift ( message ) ;
2014-09-17 14:38:33 +02:00
// Update the session storage
sessionStorage . setItem ( "history_" + $scope . room _id , JSON . stringify ( this . data ) ) ;
2014-09-17 14:18:39 +02:00
// Reset history position
this . position = - 1 ;
this . typingMessage = undefined ;
} ,
// Move in the history
go : function ( offset ) {
if ( - 1 === this . position ) {
// User starts to go to into the history, save the current line
2014-09-20 01:49:45 +02:00
this . typingMessage = $ ( '#mainInput' ) . val ( ) ;
2014-09-17 14:18:39 +02:00
}
else {
// If the user modified this line in history, keep the change
2014-09-20 01:49:45 +02:00
this . data [ this . position ] = $ ( '#mainInput' ) . val ( ) ;
2014-09-17 14:18:39 +02:00
}
// Bounds the new position to valid data
var newPosition = this . position + offset ;
newPosition = Math . max ( - 1 , newPosition ) ;
newPosition = Math . min ( newPosition , this . data . length - 1 ) ;
this . position = newPosition ;
if ( - 1 !== this . position ) {
// Show the message from the history
2014-09-20 01:49:45 +02:00
$ ( '#mainInput' ) . val ( this . data [ this . position ] ) ;
2014-09-17 14:18:39 +02:00
}
else if ( undefined !== this . typingMessage ) {
// Go back to the message the user started to type
2014-09-20 01:49:45 +02:00
$ ( '#mainInput' ) . val ( this . typingMessage ) ;
2014-09-17 14:18:39 +02:00
}
}
} ;
// Make history singleton methods available from HTML
$scope . history = {
goUp : function ( $event ) {
if ( $scope . room _id ) {
history . go ( 1 ) ;
}
$event . preventDefault ( ) ;
} ,
goDown : function ( $event ) {
if ( $scope . room _id ) {
history . go ( - 1 ) ;
}
$event . preventDefault ( ) ;
}
} ;
2014-08-12 16:10:52 +02:00
} ] ) ;