Merge branch 'develop' of github.com:matrix-org/synapse into develop
						commit
						1c7bb34ffd
					
				|  | @ -18,6 +18,7 @@ htmlcov | |||
| demo/*.db | ||||
| demo/*.log | ||||
| demo/*.pid | ||||
| demo/etc | ||||
| 
 | ||||
| graph/*.svg | ||||
| graph/*.png | ||||
|  |  | |||
|  | @ -347,11 +347,12 @@ Receiving live updates on a client | |||
| Clients can receive new events by long-polling the home server. This will hold open the | ||||
| HTTP connection for a short period of time waiting for new events, returning early if an | ||||
| event occurs. This is called the `Event Stream`_. All events which are visible to the | ||||
| client and match the client's query will appear in the event stream. When the request | ||||
| client will appear in the event stream. When the request | ||||
| returns, an ``end`` token is included in the response. This token can be used in the next | ||||
| request to continue where the client left off. | ||||
| 
 | ||||
| .. TODO | ||||
|   How do we filter the event stream? | ||||
|   Do we ever return multiple events in a single request?  Don't we get lots of request | ||||
|   setup RTT latency if we only do one event per request? Do we ever support streaming | ||||
|   requests? Why not websockets? | ||||
|  | @ -473,7 +474,9 @@ action in a room a user must have a suitable power level. | |||
| 
 | ||||
| Power levels for users are defined in ``m.room.power_levels``, where both | ||||
| a default and specific users' power levels can be set. By default all users | ||||
| have a power level of 0. | ||||
| have a power level of 0, other than the room creator whose power level defaults to 100. | ||||
| Power levels for users are tracked per-room even if the user is not present in  | ||||
| the room. | ||||
| 
 | ||||
| State events may contain a ``required_power_level`` key, which indicates the | ||||
| minimum power a user must have before they can update that state key. The only | ||||
|  | @ -483,11 +486,11 @@ To perform certain actions there are additional power level requirements | |||
| defined in the following state events: | ||||
| 
 | ||||
| - ``m.room.send_event_level`` defines the minimum level for sending non-state  | ||||
|   events. Defaults to 5. | ||||
|   events. Defaults to 50. | ||||
| - ``m.room.add_state_level`` defines the minimum level for adding new state, | ||||
|   rather than updating existing state. Defaults to 5. | ||||
|   rather than updating existing state. Defaults to 50. | ||||
| - ``m.room.ops_level`` defines the minimum levels to ban and kick other users. | ||||
|   This defaults to a kick and ban levels of 5 each. | ||||
|   This defaults to a kick and ban levels of 50 each. | ||||
| 
 | ||||
| 
 | ||||
| Joining rooms | ||||
|  | @ -1122,19 +1125,104 @@ Typing notifications | |||
| 
 | ||||
| Voice over IP | ||||
| ============= | ||||
| .. NOTE:: | ||||
|   This section is a work in progress. | ||||
| Matrix can also be used to set up VoIP calls. This is part of the core specification, | ||||
| although is still in a very early stage. Voice (and video) over Matrix is based on | ||||
| the WebRTC standards. | ||||
| 
 | ||||
| .. TODO Dave | ||||
|     - what are the event types. | ||||
|     - what are the valid keys/values. What do they represent. Any gotchas? | ||||
|     - In what sequence should the events be sent? | ||||
|     - How do you accept / decline inbound calls? How do you make outbound calls? | ||||
|       Give examples. | ||||
|     - How does negotiation work? Give examples. | ||||
|     - How do you hang up? | ||||
|     - What does call log information look like e.g. duration of call? | ||||
| Call events are sent to a room, like any other event. This means that clients | ||||
| must only send call events to rooms with exactly two participants as currently | ||||
| the WebRTC standard is based around two-party communication. | ||||
| 
 | ||||
| Events | ||||
| ------ | ||||
| ``m.call.invite`` | ||||
| This event is sent by the caller when they wish to establish a call. | ||||
| 
 | ||||
|   Required keys: | ||||
|     - ``call_id`` : "string" - A unique identifier for the call | ||||
|     - ``offer`` : "offer object" - The session description | ||||
|     - ``version`` : "integer" - The version of the VoIP specification this message | ||||
|                                 adheres to. This specification is version 0. | ||||
|        | ||||
|   Optional keys: | ||||
|     None. | ||||
|   Example: | ||||
|     ``{ "version" : 0, "call_id": "12345", "offer": { "type" : "offer", "sdp" : "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]" } }`` | ||||
| 
 | ||||
| ``Offer Object`` | ||||
|   Required keys: | ||||
|     - ``type`` : "string" - The type of session description, in this case 'offer' | ||||
|     - ``sdp`` : "string" - The SDP text of the session description | ||||
| 
 | ||||
| ``m.call.candidate`` | ||||
| This event is sent by callers after sending an invite and by the callee after answering. | ||||
| Its purpose is to give the other party an additional ICE candidate to try using to | ||||
| communicate. | ||||
| 
 | ||||
|   Required keys: | ||||
|     - ``call_id`` : "string" - The ID of the call this event relates to | ||||
|     - ``version`` : "integer" - The version of the VoIP specification this messages | ||||
|                                 adheres to. his specification is version 0. | ||||
|     - ``candidate`` : "candidate object" - Object describing the candidate. | ||||
| 
 | ||||
| ``Candidate Object`` | ||||
| 
 | ||||
|   Required Keys: | ||||
|     - ``sdpMid`` : "string" - The SDP media type this candidate is intended for. | ||||
|     - ``sdpMLineIndex`` : "integer" - The index of the SDP 'm' line this | ||||
|                                       candidate is intended for | ||||
|     - ``candidate`` : "string" - The SDP 'a' line of the candidate | ||||
| 
 | ||||
| ``m.call.answer`` | ||||
| 
 | ||||
|   Required keys: | ||||
|     - ``call_id`` : "string" - The ID of the call this event relates to | ||||
|     - ``version`` : "integer" - The version of the VoIP specification this messages | ||||
|     - ``answer`` : "answer object" - Object giving the SDK answer | ||||
| 
 | ||||
| ``Answer Object`` | ||||
| 
 | ||||
|   Required keys: | ||||
|     - ``type`` : "string" - The type of session description. 'answer' in this case. | ||||
|     - ``sdp`` : "string" - The SDP text of the session description | ||||
| 
 | ||||
| ``m.call.hangup`` | ||||
| Sent by either party to signal their termination of the call. This can be sent either once | ||||
| the call has has been established or before to abort the call. | ||||
| 
 | ||||
|   Required keys: | ||||
|     - ``call_id`` : "string" - The ID of the call this event relates to | ||||
|     - ``version`` : "integer" - The version of the VoIP specification this messages | ||||
| 
 | ||||
| Message Exchange | ||||
| ---------------- | ||||
| A call is set up with messages exchanged as follows: | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|    Caller                   Callee | ||||
|  m.call.invite -----------> | ||||
|  m.call.candidate --------> | ||||
|  [more candidates events] | ||||
|                          User answers call | ||||
|                   <------ m.call.answer | ||||
|                [...] | ||||
|                   <------ m.call.hangup | ||||
|                    | ||||
| Or a rejected call: | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|    Caller                   Callee | ||||
|  m.call.invite -----------> | ||||
|  m.call.candidate --------> | ||||
|  [more candidates events] | ||||
|                         User rejects call | ||||
|                  <------- m.call.hangup | ||||
| 
 | ||||
| Calls are negotiated according to the WebRTC specification. | ||||
|   | ||||
|   | ||||
| Profiles | ||||
| ======== | ||||
| .. NOTE:: | ||||
|  | @ -1149,8 +1237,8 @@ Profiles | |||
|   - Display name changes also generates m.room.member with displayname key f.e. room | ||||
|     the user is in. | ||||
| 
 | ||||
| Internally within Matrix users are referred to by their user ID, which is not a | ||||
| human-friendly string. Profiles grant users the ability to see human-readable  | ||||
| Internally within Matrix users are referred to by their user ID, which is typically | ||||
| a compact unique identifier. Profiles grant users the ability to see human-readable  | ||||
| names for other users that are in some way meaningful to them. Additionally,  | ||||
| profiles can publish additional information, such as the user's age or location. | ||||
| 
 | ||||
|  | @ -1464,17 +1552,19 @@ Federation is the term used to describe how to communicate between Matrix home | |||
| servers. Federation is a mechanism by which two home servers can exchange | ||||
| Matrix event messages, both as a real-time push of current events, and as a | ||||
| historic fetching mechanism to synchronise past history for clients to view. It | ||||
| uses HTTP connections between each pair of servers involved as the underlying | ||||
| uses HTTPS connections between each pair of servers involved as the underlying | ||||
| transport. Messages are exchanged between servers in real-time by active pushing | ||||
| from each server's HTTP client into the server of the other. Queries to fetch | ||||
| historic data for the purpose of back-filling scrollback buffers and the like | ||||
| can also be performed. | ||||
| can also be performed. Currently routing of messages between homeservers is full | ||||
| mesh (like email) - however, fan-out refinements to this design are currently | ||||
| under consideration. | ||||
| 
 | ||||
| There are three main kinds of communication that occur between home servers: | ||||
| 
 | ||||
| :Queries: | ||||
|    These are single request/response interactions between a given pair of | ||||
|    servers, initiated by one side sending an HTTP GET request to obtain some | ||||
|    servers, initiated by one side sending an HTTPS GET request to obtain some | ||||
|    information, and responded by the other. They are not persisted and contain | ||||
|    no long-term significant history. They simply request a snapshot state at the | ||||
|    instant the query is made. | ||||
|  | @ -1690,7 +1780,7 @@ by the same origin as the current one, or other origins. | |||
| Because of the distributed nature of participants in a Matrix conversation, it | ||||
| is impossible to establish a globally-consistent total ordering on the events. | ||||
| However, by annotating each outbound PDU at its origin with IDs of other PDUs it | ||||
| has received, a partial ordering can be constructed allowing causallity | ||||
| has received, a partial ordering can be constructed allowing causality | ||||
| relationships to be preserved. A client can then display these messages to the | ||||
| end-user in some order consistent with their content and ensure that no message | ||||
| that is semantically in reply of an earlier one is ever displayed before it. | ||||
|  | @ -1776,7 +1866,7 @@ Retrieves a sliding-window history of previous PDUs that occurred on the | |||
| given context. Starting from the PDU ID(s) given in the "v" argument, the | ||||
| PDUs that preceeded it are retrieved, up to a total number given by the | ||||
| "limit" argument. These are then returned in a new Transaction containing all | ||||
| off the PDUs. | ||||
| of the PDUs. | ||||
| 
 | ||||
| 
 | ||||
| To stream events all the events:: | ||||
|  | @ -1961,6 +2051,9 @@ The ``retry_after_ms`` key SHOULD be included to tell the client how long they h | |||
| in milliseconds before they can try again. | ||||
| 
 | ||||
| .. TODO | ||||
|   - Surely we should recommend an algorithm for the rate limiting, rather than letting every | ||||
|     homeserver come up with their own idea, causing totally unpredictable performance over | ||||
|     federated rooms? | ||||
|   - crypto (s-s auth) | ||||
|   - E2E | ||||
|   - Lawful intercept + Key Escrow | ||||
|  | @ -1971,6 +2064,9 @@ Policy Servers | |||
| .. NOTE:: | ||||
|   This section is a work in progress. | ||||
| 
 | ||||
| .. TODO | ||||
|   We should mention them in the Architecture section at least... | ||||
|    | ||||
| Content repository | ||||
| ================== | ||||
| .. NOTE:: | ||||
|  | @ -2069,6 +2165,9 @@ Transaction: | |||
|   A message which relates to the communication between a given pair of servers. | ||||
|   A transaction contains possibly-empty lists of PDUs and EDUs. | ||||
| 
 | ||||
| .. TODO | ||||
|   This glossary contradicts the terms used above - especially on State Events v. "State" | ||||
|   and Non-State Events v. "Events".  We need better consistent names. | ||||
| 
 | ||||
| .. Links through the external API docs are below | ||||
| .. ============================================= | ||||
|  | @ -2116,3 +2215,4 @@ Transaction: | |||
| .. _/join/<room_alias_or_id>: /docs/api/client-server/#!/-rooms/join | ||||
| 
 | ||||
| .. _`Event Stream`: /docs/api/client-server/#!/-events/get_event_stream | ||||
| 
 | ||||
|  |  | |||
|  | @ -79,85 +79,4 @@ angular.module('matrixWebClient') | |||
|     return function(text) { | ||||
|         return $sce.trustAsHtml(text); | ||||
|     }; | ||||
| }]) | ||||
| 
 | ||||
| // Compute the room name according to information we have
 | ||||
| .filter('roomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) { | ||||
|     return function(room_id) { | ||||
|         var roomName; | ||||
| 
 | ||||
|         // If there is an alias, use it
 | ||||
|         // TODO: only one alias is managed for now
 | ||||
|         var alias = matrixService.getRoomIdToAliasMapping(room_id); | ||||
|         if (alias) { | ||||
|             roomName = alias; | ||||
|         } | ||||
| 
 | ||||
|         if (undefined === roomName) { | ||||
|             // Else, build the name from its users
 | ||||
|             var room = $rootScope.events.rooms[room_id]; | ||||
|             if (room) { | ||||
|                 var room_name_event = room["m.room.name"]; | ||||
| 
 | ||||
|                 if (room_name_event) { | ||||
|                     roomName = room_name_event.content.name; | ||||
|                 } | ||||
|                 else if (room.members) { | ||||
|                     // Limit the room renaming to 1:1 room
 | ||||
|                     if (2 === Object.keys(room.members).length) { | ||||
|                         for (var i in room.members) { | ||||
|                             var member = room.members[i]; | ||||
|                             if (member.state_key !== matrixService.config().user_id) { | ||||
| 
 | ||||
|                                 if (member.state_key in $rootScope.presence) { | ||||
|                                     // If the user is available in presence, use the displayname there
 | ||||
|                                     // as it is the most uptodate
 | ||||
|                                     roomName = $rootScope.presence[member.state_key].content.displayname; | ||||
|                                 } | ||||
|                                 else if (member.content.displayname) { | ||||
|                                     roomName = member.content.displayname; | ||||
|                                 } | ||||
|                                 else { | ||||
|                                     roomName = member.state_key; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     else if (1 === Object.keys(room.members).length) { | ||||
|                         // The other member may be in the invite list, get all invited users
 | ||||
|                         var invitedUserIDs = []; | ||||
|                         for (var i in room.messages) { | ||||
|                             var message = room.messages[i]; | ||||
|                             if ("m.room.member" === message.type && "invite" === message.membership) { | ||||
|                                 // Make sure there is no duplicate user
 | ||||
|                                 if (-1 === invitedUserIDs.indexOf(message.state_key)) { | ||||
|                                     invitedUserIDs.push(message.state_key); | ||||
|                                 } | ||||
|                             }  | ||||
|                         } | ||||
|                          | ||||
|                         // For now, only 1:1 room needs to be renamed. It means only 1 invited user
 | ||||
|                         if (1 === invitedUserIDs.length) { | ||||
|                             var userID = invitedUserIDs[0]; | ||||
| 
 | ||||
|                             // Try to resolve his displayname in presence global data
 | ||||
|                             if (userID in $rootScope.presence) { | ||||
|                                 roomName = $rootScope.presence[userID].content.displayname; | ||||
|                             } | ||||
|                             else { | ||||
|                                 roomName = userID; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (undefined === roomName) { | ||||
|             // By default, use the room ID
 | ||||
|             roomName = room_id; | ||||
|         } | ||||
| 
 | ||||
|         return roomName; | ||||
|     }; | ||||
| }]); | ||||
| }]); | ||||
|  | @ -251,12 +251,14 @@ a:active  { color: #000; } | |||
| .userAvatar .userAvatarImage { | ||||
|     position: absolute; | ||||
|     top: 0px; | ||||
|     object-fit: cover;     | ||||
|     object-fit: cover; | ||||
|     width: 100%; | ||||
| } | ||||
| 
 | ||||
| .userAvatar .userAvatarGradient { | ||||
|     position: absolute; | ||||
|     bottom: 20px; | ||||
|     width: 100%; | ||||
| } | ||||
| 
 | ||||
| .userAvatar .userName { | ||||
|  | @ -417,6 +419,13 @@ a:active  { color: #000; } | |||
|     text-align: left ! important; | ||||
| } | ||||
| 
 | ||||
| .bubble .messagePending { | ||||
|     opacity: 0.3 | ||||
| } | ||||
| .messageUnSent { | ||||
|     color: #F00; | ||||
| } | ||||
| 
 | ||||
| #room-fullscreen-image { | ||||
|     position: absolute; | ||||
|     top: 0px; | ||||
|  |  | |||
|  | @ -0,0 +1,135 @@ | |||
| /* | ||||
|  Copyright 2014 OpenMarket Ltd | ||||
|   | ||||
|  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. | ||||
|  */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| angular.module('matrixFilter', []) | ||||
| 
 | ||||
| // Compute the room name according to information we have
 | ||||
| .filter('mRoomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) { | ||||
|     return function(room_id) { | ||||
|         var roomName; | ||||
| 
 | ||||
|         // If there is an alias, use it
 | ||||
|         // TODO: only one alias is managed for now
 | ||||
|         var alias = matrixService.getRoomIdToAliasMapping(room_id); | ||||
|         if (alias) { | ||||
|             roomName = alias; | ||||
|         } | ||||
| 
 | ||||
|         if (undefined === roomName) { | ||||
|             // Else, build the name from its users
 | ||||
|             var room = $rootScope.events.rooms[room_id]; | ||||
|             if (room) { | ||||
|                 var room_name_event = room["m.room.name"]; | ||||
| 
 | ||||
|                 if (room_name_event) { | ||||
|                     roomName = room_name_event.content.name; | ||||
|                 } | ||||
|                 else if (room.members) { | ||||
|                     // Limit the room renaming to 1:1 room
 | ||||
|                     if (2 === Object.keys(room.members).length) { | ||||
|                         for (var i in room.members) { | ||||
|                             var member = room.members[i]; | ||||
|                             if (member.state_key !== matrixService.config().user_id) { | ||||
| 
 | ||||
|                                 if (member.state_key in $rootScope.presence) { | ||||
|                                     // If the user is available in presence, use the displayname there
 | ||||
|                                     // as it is the most uptodate
 | ||||
|                                     roomName = $rootScope.presence[member.state_key].content.displayname; | ||||
|                                 } | ||||
|                                 else if (member.content.displayname) { | ||||
|                                     roomName = member.content.displayname; | ||||
|                                 } | ||||
|                                 else { | ||||
|                                     roomName = member.state_key; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     else if (1 === Object.keys(room.members).length) { | ||||
|                         // The other member may be in the invite list, get all invited users
 | ||||
|                         var invitedUserIDs = []; | ||||
|                         for (var i in room.messages) { | ||||
|                             var message = room.messages[i]; | ||||
|                             if ("m.room.member" === message.type && "invite" === message.membership) { | ||||
|                                 // Make sure there is no duplicate user
 | ||||
|                                 if (-1 === invitedUserIDs.indexOf(message.state_key)) { | ||||
|                                     invitedUserIDs.push(message.state_key); | ||||
|                                 } | ||||
|                             }  | ||||
|                         } | ||||
|                          | ||||
|                         // For now, only 1:1 room needs to be renamed. It means only 1 invited user
 | ||||
|                         if (1 === invitedUserIDs.length) { | ||||
|                             var userID = invitedUserIDs[0]; | ||||
| 
 | ||||
|                             // Try to resolve his displayname in presence global data
 | ||||
|                             if (userID in $rootScope.presence) { | ||||
|                                 roomName = $rootScope.presence[userID].content.displayname; | ||||
|                             } | ||||
|                             else { | ||||
|                                 roomName = userID; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (undefined === roomName) { | ||||
|             // By default, use the room ID
 | ||||
|             roomName = room_id; | ||||
|         } | ||||
| 
 | ||||
|         return roomName; | ||||
|     }; | ||||
| }]) | ||||
| 
 | ||||
| // Compute the user display name in a room according to the data already downloaded
 | ||||
| .filter('mUserDisplayName', ['$rootScope', function($rootScope) { | ||||
|     return function(user_id, room_id) { | ||||
|         var displayName; | ||||
|      | ||||
|         // Try to find the user name among presence data
 | ||||
|         // Warning: that means we have received before a presence event for this
 | ||||
|         // user which cannot be guaranted.
 | ||||
|         // However, if we get the info by this way, we are sure this is the latest user display name
 | ||||
|         // See FIXME comment below
 | ||||
|         if (user_id in $rootScope.presence) { | ||||
|             displayName = $rootScope.presence[user_id].content.displayname; | ||||
|         } | ||||
|              | ||||
|         // FIXME: Would like to use the display name as defined in room members of the room.
 | ||||
|         // But this information is the display name of the user when he has joined the room.
 | ||||
|         // It does not take into account user display name update
 | ||||
|         if (room_id) { | ||||
|             var room = $rootScope.events.rooms[room_id]; | ||||
|             if (room && (user_id in room.members)) { | ||||
|                 var member = room.members[user_id]; | ||||
|                 if (member.content.displayname) { | ||||
|                     displayName = member.content.displayname; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if (undefined === displayName) { | ||||
|             // By default, use the user ID
 | ||||
|             displayName = user_id; | ||||
|         } | ||||
|         return displayName; | ||||
|     }; | ||||
| }]); | ||||
|  | @ -168,18 +168,20 @@ angular.module('matrixService', []) | |||
|         }, | ||||
| 
 | ||||
|         // Change the membership of an another user
 | ||||
|         setMembership: function(room_id, user_id, membershipValue) { | ||||
|         setMembership: function(room_id, user_id, membershipValue, reason) { | ||||
|              | ||||
|             // The REST path spec
 | ||||
|             var path = "/rooms/$room_id/state/m.room.member/$user_id"; | ||||
|             path = path.replace("$room_id", encodeURIComponent(room_id)); | ||||
|             path = path.replace("$user_id", user_id); | ||||
| 
 | ||||
|             return doRequest("PUT", path, undefined, { | ||||
|                 membership: membershipValue | ||||
|                 membership : membershipValue, | ||||
|                 reason: reason | ||||
|             }); | ||||
|         }, | ||||
|             | ||||
|         // Bans a user from from a room
 | ||||
|         // Bans a user from a room
 | ||||
|         ban: function(room_id, user_id, reason) { | ||||
|             var path = "/rooms/$room_id/ban"; | ||||
|             path = path.replace("$room_id", encodeURIComponent(room_id)); | ||||
|  | @ -189,7 +191,20 @@ angular.module('matrixService', []) | |||
|                 reason: reason | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|          | ||||
|         // Unbans a user in a room
 | ||||
|         unban: function(room_id, user_id) { | ||||
|             // FIXME: To update when there will be homeserver API for unban 
 | ||||
|             // For now, do an unban by resetting the user membership to "leave"
 | ||||
|             return this.setMembership(room_id, user_id, "leave"); | ||||
|         }, | ||||
|          | ||||
|         // Kicks a user from a room
 | ||||
|         kick: function(room_id, user_id, reason) { | ||||
|             // Set the user membership to "leave" to kick him
 | ||||
|             return this.setMembership(room_id, user_id, "leave", reason); | ||||
|         }, | ||||
|          | ||||
|         // Retrieves the room ID corresponding to a room alias
 | ||||
|         resolveRoomAlias:function(room_alias) { | ||||
|             var path = "/_matrix/client/api/v1/directory/room/$room_alias"; | ||||
|  | @ -434,7 +449,7 @@ angular.module('matrixService', []) | |||
|             var path = "/presence/$user_id/status"; | ||||
|             path = path.replace("$user_id", config.user_id); | ||||
|             return doRequest("PUT", path, undefined, { | ||||
|                 state: presence | ||||
|                 presence: presence | ||||
|             }); | ||||
|         }, | ||||
|          | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ | |||
|     <script src="settings/settings-controller.js"></script> | ||||
|     <script src="user/user-controller.js"></script> | ||||
|     <script src="components/matrix/matrix-service.js"></script> | ||||
|     <script src="components/matrix/matrix-filter.js"></script> | ||||
|     <script src="components/matrix/matrix-call.js"></script> | ||||
|     <script src="components/matrix/matrix-phone-service.js"></script> | ||||
|     <script src="components/matrix/event-stream-service.js"></script> | ||||
|  |  | |||
|  | @ -39,8 +39,8 @@ | |||
|                         Only http://matrix.org:8090 currently exists.</div> | ||||
|                     <br/> | ||||
|                     <br/> | ||||
|                     <a href="#/register" style="padding-right: 3em">Create account</a> | ||||
|                     <a href="#/reset_password">Forgotten password?</a> | ||||
|                     <a href="#/register" style="padding-right: 0em">Create account</a> | ||||
|                     <a href="#/reset_password" style="display: none; ">Forgotten password?</a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </form> | ||||
|  |  | |||
|  | @ -82,7 +82,7 @@ angular.module('RegisterController', ['matrixService']) | |||
|                 } | ||||
|             ); | ||||
|         } else { | ||||
|             registerWithMxidAndPassword($scope.account.desired_user_id, $scope.account.pwd1); | ||||
|             $scope.registerWithMxidAndPassword($scope.account.desired_user_id, $scope.account.pwd1); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ | |||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| angular.module('RecentsController', ['matrixService', 'eventHandlerService']) | ||||
| angular.module('RecentsController', ['matrixService', 'matrixFilter', 'eventHandlerService']) | ||||
| .controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService',  | ||||
|                                function($scope,  matrixService, eventHandlerService) { | ||||
|     $scope.rooms = {}; | ||||
|  | @ -28,13 +28,8 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService']) | |||
|     var listenToEventStream = function() { | ||||
|         // Refresh the list on matrix invitation and message event
 | ||||
|         $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) { | ||||
|             var config = matrixService.config(); | ||||
|             if (isLive && event.state_key === config.user_id && event.content.membership === "invite") { | ||||
|                 console.log("Invited to room " + event.room_id); | ||||
|                 // FIXME push membership to top level key to match /im/sync
 | ||||
|                 event.membership = event.content.membership; | ||||
| 
 | ||||
|                 $scope.rooms[event.room_id] = event; | ||||
|             if (isLive) { | ||||
|                 $scope.rooms[event.room_id].lastMsg = event; | ||||
|             } | ||||
|         }); | ||||
|         $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) { | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|                ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}"> | ||||
|             <tr> | ||||
|                 <td class="recentsRoomName"> | ||||
|                     {{ room.room_id | roomName }} | ||||
|                     {{ room.room_id | mRoomName }} | ||||
|                 </td> | ||||
|                 <td class="recentsRoomSummaryTS"> | ||||
|                     {{ (room.lastMsg.ts) | date:'MMM d HH:mm' }} | ||||
|  | @ -16,27 +16,48 @@ | |||
|             <tr> | ||||
|                 <td colspan="2" class="recentsRoomSummary"> | ||||
| 
 | ||||
|                     <div ng-show="room.membership === 'invite'" > | ||||
|                         {{ room.inviter }} invited you | ||||
|                     <div ng-show="room.membership === 'invite'"> | ||||
|                         {{ room.lastMsg.inviter | mUserDisplayName: room.room_id }} invited you | ||||
|                     </div> | ||||
| 
 | ||||
|                     <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" > | ||||
|                          <div ng-switch-when="m.room.member"> | ||||
|                             {{ room.lastMsg.user_id }} | ||||
|                             {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }} | ||||
|                             {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }} | ||||
|                      | ||||
|                     <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type"> | ||||
|                         <div ng-switch-when="m.room.member"> | ||||
|                             <span ng-if="'join' === room.lastMsg.content.membership"> | ||||
|                                 {{ room.lastMsg.state_key | mUserDisplayName: room.room_id}} joined | ||||
|                             </span> | ||||
|                             <span ng-if="'leave' === room.lastMsg.content.membership"> | ||||
|                                 <span ng-if="room.lastMsg.user_id === room.lastMsg.state_key"> | ||||
|                                     {{room.lastMsg.state_key | mUserDisplayName: room.room_id }} left | ||||
|                                 </span> | ||||
|                                 <span ng-if="room.lastMsg.user_id !== room.lastMsg.state_key"> | ||||
|                                     {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} | ||||
|                                     {{ {"join": "kicked", "ban": "unbanned"}[room.lastMsg.content.prev] }} | ||||
|                                     {{ room.lastMsg.state_key | mUserDisplayName: room.room_id }} | ||||
|                                 </span> | ||||
|                                 <span ng-if="'join' === room.lastMsg.content.prev && room.lastMsg.content.reason"> | ||||
|                                     : {{ room.lastMsg.content.reason }} | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                             <span ng-if="'invite' === room.lastMsg.content.membership || 'ban' === room.lastMsg.content.membership"> | ||||
|                                 {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} | ||||
|                                 {{ {"invite": "invited", "ban": "banned"}[room.lastMsg.content.membership] }} | ||||
|                                 {{ room.lastMsg.state_key | mUserDisplayName: room.room_id }} | ||||
|                                 <span ng-if="'ban' === room.lastMsg.content.prev && room.lastMsg.content.reason"> | ||||
|                                     : {{ room.lastMsg.content.reason }} | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div ng-switch-when="m.room.message"> | ||||
|                             <div ng-switch="room.lastMsg.content.msgtype"> | ||||
|                                 <div ng-switch-when="m.text"> | ||||
|                                     {{ room.lastMsg.user_id }} : | ||||
|                                     {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} : | ||||
|                                     <span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'"> | ||||
|                                     </span> | ||||
|                                 </div> | ||||
| 
 | ||||
|                                 <div ng-switch-when="m.image"> | ||||
|                                     {{ room.lastMsg.user_id }} sent an image | ||||
|                                     {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} sent an image | ||||
|                                 </div> | ||||
| 
 | ||||
|                                 <div ng-switch-when="m.emote"> | ||||
|  | @ -51,7 +72,7 @@ | |||
|                         </div> | ||||
| 
 | ||||
|                         <div ng-switch-default> | ||||
|                             <div ng-if="room.lastMsg.type.indexOf('m.call.') == 0"> | ||||
|                             <div ng-if="room.lastMsg.type.indexOf('m.call.') === 0"> | ||||
|                                 Call | ||||
|                             </div> | ||||
|                         </div> | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| angular.module('RoomController', ['ngSanitize', 'mFileInput']) | ||||
| angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) | ||||
| .controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall', | ||||
|                                function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) { | ||||
|    'use strict'; | ||||
|  | @ -32,9 +32,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|         first_pagination: true, // this is toggled off when the first pagination is done
 | ||||
|         can_paginate: true, // this is toggled off when we run out of items
 | ||||
|         paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
 | ||||
|         stream_failure: undefined, // the response when the stream fails
 | ||||
|         // FIXME: sending has been disabled, as surely messages should be sent in the background rather than locking the UI synchronously --Matthew
 | ||||
|         sending: false // true when a message is being sent. It helps to disable the UI when a process is running
 | ||||
|         stream_failure: undefined // the response when the stream fails
 | ||||
|     }; | ||||
|     $scope.members = {}; | ||||
|     $scope.autoCompleting = false; | ||||
|  | @ -44,18 +42,25 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|     $scope.imageURLToSend = ""; | ||||
|     $scope.userIDToInvite = ""; | ||||
|      | ||||
|     var scrollToBottom = function() { | ||||
|     var scrollToBottom = function(force) { | ||||
|         console.log("Scrolling to bottom"); | ||||
|         $timeout(function() { | ||||
|             var objDiv = document.getElementById("messageTableWrapper"); | ||||
|             objDiv.scrollTop = objDiv.scrollHeight; | ||||
|         }, 0); | ||||
|          | ||||
|         // 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; | ||||
|             }, 0); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) { | ||||
|         if (isLive && event.room_id === $scope.room_id) { | ||||
|             scrollToBottom(); | ||||
|              | ||||
|             scrollToBottom(); | ||||
| 
 | ||||
|             if (window.Notification) { | ||||
|                 // Show notification when the user is idle
 | ||||
|                 if (matrixService.presence.offline === mPresence.getState()) { | ||||
|  | @ -76,6 +81,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|      | ||||
|     $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) { | ||||
|         if (isLive) { | ||||
|             scrollToBottom(); | ||||
|             updateMemberList(event); | ||||
|         } | ||||
|     }); | ||||
|  | @ -169,16 +175,18 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|     var updateMemberList = function(chunk) { | ||||
|         if (chunk.room_id != $scope.room_id) return; | ||||
| 
 | ||||
|         // Ignore banned and kicked (leave) people
 | ||||
|         if ("ban" === chunk.membership || "leave" === chunk.membership) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // set target_user_id to keep things clear
 | ||||
|         var target_user_id = chunk.state_key; | ||||
| 
 | ||||
|         var isNewMember = !(target_user_id in $scope.members); | ||||
|         if (isNewMember) { | ||||
|              | ||||
|             // Ignore banned and kicked (leave) people
 | ||||
|             if ("ban" === chunk.membership || "leave" === chunk.membership) { | ||||
|                 return; | ||||
|             } | ||||
|          | ||||
|             // FIXME: why are we copying these fields around inside chunk?
 | ||||
|             if ("presence" in chunk.content) { | ||||
|                 chunk.presence = chunk.content.presence; | ||||
|  | @ -202,6 +210,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|         } | ||||
|         else { | ||||
|             // selectively update membership and presence else it will nuke the picture and displayname too :/
 | ||||
|              | ||||
|             // Remove banned and kicked (leave) people
 | ||||
|             if ("ban" === chunk.membership || "leave" === chunk.membership) { | ||||
|                 delete $scope.members[target_user_id]; | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             var member = $scope.members[target_user_id]; | ||||
|             member.membership = chunk.content.membership; | ||||
|             if ("presence" in chunk.content) { | ||||
|  | @ -256,7 +271,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|              | ||||
|             normaliseMembersPowerLevels(); | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     // 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
 | ||||
|  | @ -277,104 +292,185 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|                 member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     $scope.send = function() { | ||||
|         if ($scope.textInput === "") { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $scope.state.sending = true; | ||||
|          | ||||
|         scrollToBottom(true); | ||||
|          | ||||
|         var promise; | ||||
|         var isCmd = false; | ||||
|          | ||||
|         // Check for IRC style commands first
 | ||||
|         if ($scope.textInput.indexOf("/") === 0) { | ||||
|             var args = $scope.textInput.split(' '); | ||||
|             var cmd = args[0]; | ||||
|         var line = $scope.textInput; | ||||
|          | ||||
|         // trim any trailing whitespace, as it can confuse the parser for IRC-style commands
 | ||||
|         line = line.replace(/\s+$/, ""); | ||||
|          | ||||
|         if (line[0] === "/" && line[1] !== "/") { | ||||
|             isCmd = true; | ||||
|              | ||||
|             var bits = line.match(/^(\S+?)( +(.*))?$/); | ||||
|             var cmd = bits[1]; | ||||
|             var args = bits[3]; | ||||
|              | ||||
|             console.log("cmd: " + cmd + ", args: " + args); | ||||
|              | ||||
|             switch (cmd) { | ||||
|                 case "/me": | ||||
|                     var emoteMsg = args.slice(1).join(' '); | ||||
|                     promise = matrixService.sendEmoteMessage($scope.room_id, emoteMsg); | ||||
|                     promise = matrixService.sendEmoteMessage($scope.room_id, args); | ||||
|                     break; | ||||
|                      | ||||
|                 case "/nick": | ||||
|                     // Change user display name
 | ||||
|                     if (2 === args.length) { | ||||
|                         promise = matrixService.setDisplayName(args[1]); | ||||
|                     if (args) { | ||||
|                         promise = matrixService.setDisplayName(args);                      | ||||
|                     } | ||||
|                     else { | ||||
|                         $scope.feedback = "Usage: /nick <display_name>"; | ||||
|                     } | ||||
|                     break; | ||||
|                      | ||||
|                 case "/kick": | ||||
|                     // Kick a user from the room
 | ||||
|                     if (2 === args.length) { | ||||
|                         var user_id = args[1]; | ||||
| 
 | ||||
|                         // Set his state in the room as leave
 | ||||
|                         promise = matrixService.setMembership($scope.room_id, user_id, "leave"); | ||||
|                     } | ||||
|                     break; | ||||
|                      | ||||
|                 case "/ban": | ||||
|                     // Ban a user from the room
 | ||||
|                     if (2 <= args.length) { | ||||
|                         // TODO: The user may have entered the display name
 | ||||
|                         // Need display name -> user_id resolution. Pb: how to manage user with same display names?
 | ||||
|                         var user_id = args[1]; | ||||
| 
 | ||||
|                         // Does the user provide a reason?
 | ||||
|                         if (3 <= args.length) { | ||||
|                             var reason = args.slice(2).join(' '); | ||||
|                     // Kick a user from the room with an optional reason
 | ||||
|                     if (args) { | ||||
|                         var matches = args.match(/^(\S+?)( +(.*))?$/); | ||||
|                         if (matches) { | ||||
|                             promise = matrixService.kick($scope.room_id, matches[1], matches[3]); | ||||
|                         } | ||||
|                         promise = matrixService.ban($scope.room_id, user_id, reason); | ||||
|                     } | ||||
| 
 | ||||
|                     if (!promise) { | ||||
|                         $scope.feedback = "Usage: /kick <userId> [<reason>]"; | ||||
|                     } | ||||
|                     break; | ||||
| 
 | ||||
|                 case "/ban": | ||||
|                     // 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]); | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     if (!promise) { | ||||
|                         $scope.feedback = "Usage: /ban <userId> [<reason>]"; | ||||
|                     } | ||||
|                     break; | ||||
| 
 | ||||
|                 case "/unban": | ||||
|                     // Unban a user from the room
 | ||||
|                     if (2 === args.length) { | ||||
|                         var user_id = args[1]; | ||||
| 
 | ||||
|                         // Reset the user membership to leave to unban him
 | ||||
|                         promise = matrixService.setMembership($scope.room_id, user_id, "leave"); | ||||
|                     if (args) { | ||||
|                         var matches = args.match(/^(\S+)$/); | ||||
|                         if (matches) { | ||||
|                             // Reset the user membership to "leave" to unban him
 | ||||
|                             promise = matrixService.unban($scope.room_id, matches[1]); | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     if (!promise) { | ||||
|                         $scope.feedback = "Usage: /unban <userId>"; | ||||
|                     } | ||||
|                     break; | ||||
|                      | ||||
|                 case "/op": | ||||
|                     // Define the power level of a user
 | ||||
|                     if (3 === args.length) { | ||||
|                         var user_id = args[1]; | ||||
|                         var powerLevel = parseInt(args[2]); | ||||
|                         promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel); | ||||
|                     if (args) { | ||||
|                         var matches = args.match(/^(\S+?)( +(\d+))?$/); | ||||
|                         var powerLevel = 50; // default power level for op
 | ||||
|                         if (matches) { | ||||
|                             var user_id = matches[1]; | ||||
|                             if (matches.length === 4) { | ||||
|                                 powerLevel = parseInt(matches[3]); | ||||
|                             } | ||||
|                             if (powerLevel !== NaN) { | ||||
|                                 promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     if (!promise) { | ||||
|                         $scope.feedback = "Usage: /op <userId> [<power level>]"; | ||||
|                     } | ||||
|                     break; | ||||
|                      | ||||
|                 case "/deop": | ||||
|                     // Reset the power level of a user
 | ||||
|                     if (2 === args.length) { | ||||
|                         var user_id = args[1]; | ||||
|                         promise = matrixService.setUserPowerLevel($scope.room_id, user_id, undefined); | ||||
|                     if (args) { | ||||
|                         var matches = args.match(/^(\S+)$/); | ||||
|                         if (matches) { | ||||
|                             promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined); | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     if (!promise) { | ||||
|                         $scope.feedback = "Usage: /deop <userId>"; | ||||
|                     } | ||||
|                     break; | ||||
|                  | ||||
|                 default: | ||||
|                     $scope.feedback = ("Unrecognised IRC-style command: " + cmd); | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if (!promise) { | ||||
|             // Send the text message
 | ||||
|             promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput); | ||||
|         // By default send this as a message unless it's an IRC-style command
 | ||||
|         if (!promise && !isCmd) { | ||||
|             var message = $scope.textInput; | ||||
|             $scope.textInput = ""; | ||||
| 
 | ||||
|             // 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: { | ||||
|                     body: message, | ||||
|                     hsob_ts: "Sending...",      // Hack timestamp to display this text in place of the message time
 | ||||
|                     msgtype: "m.text" | ||||
|                 }, | ||||
|                 room_id: $scope.room_id, | ||||
|                 type: "m.room.message", | ||||
|                 user_id: $scope.state.user_id, | ||||
|                 echo_msg_state: "messagePending"     // Add custom field to indicate the state of this fake message to HTML
 | ||||
|             }; | ||||
| 
 | ||||
|             $rootScope.events.rooms[$scope.room_id].messages.push(echoMessage); | ||||
|             scrollToBottom(); | ||||
| 
 | ||||
|             // Make the request
 | ||||
|             promise = matrixService.sendTextMessage($scope.room_id, message); | ||||
|         } | ||||
| 
 | ||||
|         if (promise) { | ||||
|             promise.then( | ||||
|                 function() { | ||||
|                     console.log("Request successfully sent"); | ||||
| 
 | ||||
|                     if (echoMessage) { | ||||
|                         // Remove the fake echo message from the room messages
 | ||||
|                         // It will be replaced by the one acknowledged by the server
 | ||||
|                         var index = $rootScope.events.rooms[$scope.room_id].messages.indexOf(echoMessage); | ||||
|                         if (index > -1) { | ||||
|                             $rootScope.events.rooms[$scope.room_id].messages.splice(index, 1); | ||||
|                         } | ||||
|                     } | ||||
|                     else { | ||||
|                         $scope.textInput = ""; | ||||
|                     } | ||||
|                 }, | ||||
|                 function(error) { | ||||
|                     $scope.feedback = "Request failed: " + error.data.error; | ||||
| 
 | ||||
|                     if (echoMessage) { | ||||
|                         // Mark the message as unsent for the rest of the page life
 | ||||
|                         echoMessage.content.hsob_ts = "Unsent"; | ||||
|                         echoMessage.echo_msg_state = "messageUnSent"; | ||||
|                     } | ||||
|                 }); | ||||
|         } | ||||
|          | ||||
|         promise.then( | ||||
|             function() { | ||||
|                 console.log("Request successfully sent"); | ||||
|                 $scope.textInput = ""; | ||||
|                 $scope.state.sending = false; | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Request failed: " + error.data.error; | ||||
|                 $scope.state.sending = false; | ||||
|             }); | ||||
|     }; | ||||
| 
 | ||||
|     $scope.onInit = function() { | ||||
|  | @ -531,25 +627,20 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|     }; | ||||
| 
 | ||||
|     $scope.sendImage = function(url, body) { | ||||
|         $scope.state.sending = true; | ||||
| 
 | ||||
|         scrollToBottom(true); | ||||
|          | ||||
|         matrixService.sendImageMessage($scope.room_id, url, body).then( | ||||
|             function() { | ||||
|                 console.log("Image sent"); | ||||
|                 $scope.state.sending = false; | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Failed to send image: " + error.data.error; | ||||
|                 $scope.state.sending = false; | ||||
|             }); | ||||
|     }; | ||||
|      | ||||
|     $scope.imageFileToSend; | ||||
|     $scope.$watch("imageFileToSend", function(newValue, oldValue) { | ||||
|         if ($scope.imageFileToSend) { | ||||
| 
 | ||||
|             $scope.state.sending = true; | ||||
| 
 | ||||
|             // Upload this image with its thumbnail to Internet
 | ||||
|             mFileUpload.uploadImageAndThumbnail($scope.imageFileToSend, THUMBNAIL_SIZE).then( | ||||
|                 function(imageMessage) { | ||||
|  | @ -557,16 +648,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|                     matrixService.sendMessage($scope.room_id, undefined, imageMessage).then( | ||||
|                         function() { | ||||
|                             console.log("Image message sent"); | ||||
|                             $scope.state.sending = false; | ||||
|                         }, | ||||
|                         function(error) { | ||||
|                             $scope.feedback = "Failed to send image message: " + error.data.error; | ||||
|                             $scope.state.sending = false; | ||||
|                         }); | ||||
|                 }, | ||||
|                 function(error) { | ||||
|                     $scope.feedback = "Can't upload image"; | ||||
|                     $scope.state.sending = false; | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
|  | @ -582,6 +670,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|         call.onHangup = $rootScope.onCallHangup; | ||||
|         call.placeCall(); | ||||
|         $rootScope.currentCall = call; | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
| }]); | ||||
|  |  | |||
|  | @ -48,6 +48,9 @@ angular.module('RoomController') | |||
|                 var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text); | ||||
|                 if (targetIndex === 0) { | ||||
|                     element[0].value = text; | ||||
|                      | ||||
|                     // Force angular to wake up and update the input ng-model by firing up input event
 | ||||
|                     angular.element(element[0]).triggerHandler('input'); | ||||
|                 } | ||||
|                 else if (search && search[1]) { | ||||
|                     // console.log("search found: " + search);
 | ||||
|  | @ -81,7 +84,10 @@ angular.module('RoomController') | |||
|                             expansion += " "; | ||||
|                         element[0].value = text.replace(/@?([a-zA-Z0-9_\-:\.]+)$/, expansion); | ||||
|                         // cancel blink
 | ||||
|                         element[0].className = "";                         | ||||
|                         element[0].className = "";      | ||||
|                          | ||||
|                         // Force angular to wake up and update the input ng-model by firing up input event
 | ||||
|                         angular.element(element[0]).triggerHandler('input'); | ||||
|                     } | ||||
|                     else { | ||||
|                         // console.log("wrapped!");
 | ||||
|  | @ -91,6 +97,9 @@ angular.module('RoomController') | |||
|                         }, 150); | ||||
|                         element[0].value = text; | ||||
|                         scope.tabCompleteIndex = 0; | ||||
|                          | ||||
|                         // Force angular to wake up and update the input ng-model by firing up input event
 | ||||
|                         angular.element(element[0]).triggerHandler('input'); | ||||
|                     } | ||||
|                 } | ||||
|                 else { | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
|     <div id="roomHeader"> | ||||
|         <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> | ||||
|         <div id="roomName"> | ||||
|             {{ room_id  | roomName }} | ||||
|             {{ room_id  | mRoomName }} | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|  | @ -40,7 +40,10 @@ | |||
|                 ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item> | ||||
|                 <td class="leftBlock"> | ||||
|                     <div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div> | ||||
|                     <div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div> | ||||
|                     <div class="timestamp" | ||||
|                          ng-class="msg.echo_msg_state"> | ||||
|                         {{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }} | ||||
|                     </div> | ||||
|                 </td> | ||||
|                 <td class="avatar"> | ||||
|                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" | ||||
|  | @ -59,15 +62,24 @@ | |||
|                                 {{ members[msg.user_id].displayname || msg.user_id }} | ||||
|                                 {{ {"join": "kicked", "ban": "unbanned"}[msg.content.prev] }} | ||||
|                                 {{ members[msg.state_key].displayname || msg.state_key }} | ||||
|                                 <span ng-if="'join' === msg.content.prev && msg.content.reason"> | ||||
|                                     : {{ msg.content.reason }} | ||||
|                                 </span> | ||||
|                             </span> | ||||
|                         </span> | ||||
|                         <span ng-if="'invite' === msg.content.membership || 'ban' === msg.content.membership"> | ||||
|                             {{ members[msg.user_id].displayname || msg.user_id }} | ||||
|                             {{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }} | ||||
|                             {{ members[msg.state_key].displayname || msg.state_key }} | ||||
|                         </span> | ||||
|                             <span ng-if="'ban' === msg.content.prev && msg.content.reason"> | ||||
|                                 : {{ msg.content.reason }} | ||||
|                             </span> | ||||
|                         </span>                         | ||||
|                          | ||||
|                         <span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/> | ||||
|                         <span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/> | ||||
|                         <span ng-show='msg.content.msgtype === "m.text"'  | ||||
|                               ng-class="msg.echo_msg_state" | ||||
|                               ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/> | ||||
|                         <div ng-show='msg.content.msgtype === "m.image"'> | ||||
|                             <div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}"> | ||||
|                                 <img class="image" ng-src="{{ msg.content.url }}"/> | ||||
|  |  | |||
|  | @ -19,6 +19,17 @@ limitations under the License. | |||
| angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInput']) | ||||
| .controller('SettingsController', ['$scope', 'matrixService', 'mFileUpload', | ||||
|                               function($scope, matrixService, mFileUpload) {                  | ||||
|     // XXX: duplicated from register
 | ||||
|     var generateClientSecret = function() { | ||||
|         var ret = ""; | ||||
|         var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||||
| 
 | ||||
|         for (var i = 0; i < 32; i++) { | ||||
|             ret += chars.charAt(Math.floor(Math.random() * chars.length)); | ||||
|         } | ||||
| 
 | ||||
|         return ret; | ||||
|     }; | ||||
|     $scope.config = matrixService.config(); | ||||
| 
 | ||||
|     $scope.profile = { | ||||
|  | @ -106,16 +117,22 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu | |||
|     $scope.linkedEmails = { | ||||
|         linkNewEmail: "", // the email entry box
 | ||||
|         emailBeingAuthed: undefined, // to populate verification text
 | ||||
|         authTokenId: undefined, // the token id from the IS
 | ||||
|         authSid: undefined, // the token id from the IS
 | ||||
|         emailCode: "", // the code entry box
 | ||||
|         linkedEmailList: matrixService.config().emailList // linked email list
 | ||||
|     }; | ||||
|      | ||||
|     $scope.linkEmail = function(email) { | ||||
|         matrixService.linkEmail(email).then( | ||||
|         if (email != $scope.linkedEmails.emailBeingAuthed) { | ||||
|             $scope.linkedEmails.emailBeingAuthed = email; | ||||
|             $scope.clientSecret = generateClientSecret(); | ||||
|             $scope.sendAttempt = 0; | ||||
|         } | ||||
|         $scope.sendAttempt++; | ||||
|         matrixService.linkEmail(email, $scope.clientSecret, $scope.sendAttempt).then( | ||||
|             function(response) { | ||||
|                 if (response.data.success === true) { | ||||
|                     $scope.linkedEmails.authTokenId = response.data.tokenId; | ||||
|                     $scope.linkedEmails.authSid = response.data.sid; | ||||
|                     $scope.emailFeedback = "You have been sent an email."; | ||||
|                     $scope.linkedEmails.emailBeingAuthed = email; | ||||
|                 } | ||||
|  | @ -129,34 +146,44 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu | |||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     $scope.submitEmailCode = function(code) { | ||||
|         var tokenId = $scope.linkedEmails.authTokenId; | ||||
|     $scope.submitEmailCode = function() { | ||||
|         var tokenId = $scope.linkedEmails.authSid; | ||||
|         if (tokenId === undefined) { | ||||
|             $scope.emailFeedback = "You have not requested a code with this email."; | ||||
|             return; | ||||
|         } | ||||
|         matrixService.authEmail(matrixService.config().user_id, tokenId, code).then( | ||||
|         matrixService.authEmail($scope.clientSecret, $scope.linkedEmails.authSid, $scope.linkedEmails.emailCode).then( | ||||
|             function(response) { | ||||
|                 if ("success" in response.data && response.data.success === false) { | ||||
|                 if ("errcode" in response.data) { | ||||
|                     $scope.emailFeedback = "Failed to authenticate email."; | ||||
|                     return; | ||||
|                 } | ||||
|                 var config = matrixService.config(); | ||||
|                 var emailList = {}; | ||||
|                 if ("emailList" in config) { | ||||
|                     emailList = config.emailList; | ||||
|                 } | ||||
|                 emailList[response.address] = response; | ||||
|                 // save the new email list
 | ||||
|                 config.emailList = emailList; | ||||
|                 matrixService.setConfig(config); | ||||
|                 matrixService.saveConfig(); | ||||
|                 // invalidate the email being authed and update UI.
 | ||||
|                 $scope.linkedEmails.emailBeingAuthed = undefined; | ||||
|                 $scope.emailFeedback = ""; | ||||
|                 $scope.linkedEmails.linkedEmailList = emailList; | ||||
|                 $scope.linkedEmails.linkNewEmail = ""; | ||||
|                 $scope.linkedEmails.emailCode = ""; | ||||
|                 matrixService.bindEmail(matrixService.config().user_id, tokenId, $scope.clientSecret).then( | ||||
|                     function(response) { | ||||
|                          if ('errcode' in response.data) { | ||||
|                              $scope.emailFeedback = "Failed to link email."; | ||||
|                              return; | ||||
|                          } | ||||
|                          var config = matrixService.config(); | ||||
|                          var emailList = {}; | ||||
|                          if ("emailList" in config) { | ||||
|                              emailList = config.emailList; | ||||
|                          } | ||||
|                          emailList[$scope.linkedEmails.emailBeingAuthed] = response; | ||||
|                          // save the new email list
 | ||||
|                          config.emailList = emailList; | ||||
|                          matrixService.setConfig(config); | ||||
|                          matrixService.saveConfig(); | ||||
|                          // invalidate the email being authed and update UI.
 | ||||
|                          $scope.linkedEmails.emailBeingAuthed = undefined; | ||||
|                          $scope.emailFeedback = ""; | ||||
|                          $scope.linkedEmails.linkedEmailList = emailList; | ||||
|                          $scope.linkedEmails.linkNewEmail = ""; | ||||
|                          $scope.linkedEmails.emailCode = ""; | ||||
|                     }, function(reason) { | ||||
|                         $scope.emailFeedback = "Failed to link email: " + reason; | ||||
|                     } | ||||
|                 ); | ||||
|             }, | ||||
|             function(reason) { | ||||
|                 $scope.emailFeedback = "Failed to auth email: " + reason; | ||||
|  | @ -182,4 +209,4 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu | |||
|             $scope.settings.notifications = permission; | ||||
|         }); | ||||
|     }; | ||||
| }]); | ||||
| }]); | ||||
|  |  | |||
|  | @ -23,14 +23,14 @@ | |||
|         </div> | ||||
|         <br/> | ||||
| 
 | ||||
|         <h3 style="display: none; ">Linked emails</h3> | ||||
|         <div class="section" style="display: none; "> | ||||
|         <h3>Linked emails</h3> | ||||
|         <div class="section"> | ||||
|             <form> | ||||
|                 <input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" /> | ||||
|                 <button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)"> | ||||
|                     Link Email | ||||
|                 </button> | ||||
|                 {{ emailFeedback }}     | ||||
|                 {{ emailFeedback }} | ||||
|             </form> | ||||
|             <form ng-hide="!linkedEmails.emailBeingAuthed"> | ||||
|                 Enter validation token for {{ linkedEmails.emailBeingAuthed }}: | ||||
|  | @ -81,7 +81,7 @@ | |||
|             <ul> | ||||
|                 <li>/nick <display_name>: change your display name</li> | ||||
|                 <li>/me <action>: send the action you are doing. /me will be replaced by your display name</li> | ||||
|                 <li>/kick <user_id>: kick the user</li> | ||||
|                 <li>/kick <user_id> [<reason>]: kick the user</li> | ||||
|                 <li>/ban <user_id> [<reason>]: ban the user</li> | ||||
|                 <li>/unban <user_id>: unban the user</li> | ||||
|                 <li>/op <user_id> <power_level>: set user power level</li> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Erik Johnston
						Erik Johnston