diff --git a/docs/specification.rst b/docs/specification.rst
index fa085bac27..c1559c886c 100644
--- a/docs/specification.rst
+++ b/docs/specification.rst
@@ -5,16 +5,20 @@ TODO(Introduction) : Matthew
- Similar to intro paragraph from README.
- Explaining the overall mission, what this spec describes...
- "What is Matrix?"
+ - Draw parallels with email?
Architecture
============
-- Basic structure: What are clients/home servers and what are their
- responsibilities? What are events.
+Clients transmit data to other clients through home servers (HSes). Clients do not communicate with each
+other directly.
::
- { Matrix clients } { Matrix clients }
+ How data flows between clients
+ ==============================
+
+ { Matrix client A } { Matrix client B }
^ | ^ |
| events | | events |
| V | V
@@ -22,22 +26,128 @@ Architecture
| |---------( HTTP )---------->| |
| Home Server | | Home Server |
| |<--------( HTTP )-----------| |
- +------------------+ +------------------+
+ +------------------+ Federation +------------------+
+
+A "Client" is an end-user, typically a human using a web application or mobile app. Clients use the
+"Client-to-Server" (C-S) API to communicate with their home server. A single Client is usually
+responsible for a single user account. A user account is represented by their "User ID". This ID is
+namespaced to the home server which allocated the account and looks like::
+
+ @localpart:domain
+
+The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user.
+
+
+A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes.
+It is typically responsible for multiple clients. "Federation" is the term used to describe the
+sharing of data between two or more home servers.
+
+Data in Matrix is encapsulated in an "Event". An event is an action within the system. Typically each
+action (e.g. sending a message) correlates with exactly one event. Each event has a ``type`` which is
+used to differentiate different kinds of data. ``type`` values SHOULD be namespaced according to standard
+Java package naming conventions, e.g. ``com.example.myapp.event``. Events are usually sent in the context
+of a "Room".
+
+Room structure
+--------------
+
+A room is a conceptual place where users can send and receive events. Rooms
+can be created, joined and left. Events are sent to a room, and all
+participants in that room will receive the event. Rooms are uniquely
+identified via a "Room ID", which look like::
+
+ !opaque_id:domain
+
+There is exactly one room ID for each room. Whilst the room ID does contain a
+domain, it is simply for namespacing room IDs. The room does NOT reside on the
+domain specified. Room IDs are not meant to be human readable.
+
+The following diagram shows an ``m.room.message`` event being sent in the room
+``!qporfwt:matrix.org``::
+
+ { @alice:matrix.org } { @bob:domain.com }
+ | ^
+ | |
+ Room ID: !qporfwt:matrix.org Room ID: !qporfwt:matrix.org
+ Event type: m.room.message Event type: m.room.message
+ Content: { JSON object } Content: { JSON object }
+ | |
+ V |
+ +------------------+ +------------------+
+ | Home Server | | Home Server |
+ | matrix.org |<-------Federation------->| domain.com |
+ +------------------+ +------------------+
+ | ................................. |
+ |______| Partially Shared State |_______|
+ | Room ID: !qporfwt:matrix.org |
+ | Servers: matrix.org, domain.com |
+ | Members: |
+ | - @alice:matrix.org |
+ | - @bob:domain.com |
+ |.................................|
+
+Federation maintains shared state between multiple home servers, such that when an event is
+sent to a room, the home server knows where to forward the event on to, and how to process
+the event. Home servers do not need to have completely shared state in order to participate
+in a room. State is scoped to a single room, and federation ensures that all home servers
+have the information they need, even if that means the home server has to request more
+information from another home server before processing the event.
+
+Room Aliases
+------------
+
+Each room can also have multiple "Room Aliases", which looks like::
+
+ #room_alias:domain
+
+A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained
+by visiting the domain specified. Room aliases are designed to be human readable strings
+which can be used to publicise rooms. Note that the mapping from a room alias to a
+room ID is not fixed, and may change over time to point to a different room ID. For this
+reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on
+subsequent requests.
+
+::
+
+ GET
+ #matrix:domain.com !aaabaa:matrix.org
+ | ^
+ | |
+ _______V____________________|____
+ | domain.com |
+ | Mappings: |
+ | #matrix >> !aaabaa:matrix.org |
+ | #golf >> !wfeiofh:sport.com |
+ | #bike >> !4rguxf:matrix.org |
+ |________________________________|
+
-- How do identity servers fit in? 3PIDs? Users? Aliases
-- Pattern of the APIs (HTTP/JSON, REST + txns)
-- Standard error response format.
-- C-S Event stream
+Identity
+--------
+- Identity in relation to 3PIDs. Discovery of users based on 3PIDs.
+- Identity servers; trusted clique of servers which replicate content.
+- They govern the mapping of 3PIDs to user IDs and the creation of said mappings.
+- Not strictly required in order to communicate.
-Rooms
-=====
-A room is a conceptual place where users can send and receive messages. Rooms
-can be created, joined and left. Messages are sent to a room, and all
-participants in that room will receive the message. Rooms are uniquely
-identified via a room ID. There is exactly one room ID for each room.
+API Standards
+-------------
+- All HTTP[S]
+- Uses JSON as HTTP bodies
+- Standard error response format { errcode: M_WHATEVER, error: "some message" }
+- C-S API provides POST for operations, or PUT with txn IDs. Explain txn IDs.
+
+Receiving live updates on a client
+----------------------------------
+- C-S longpoll event stream
+- Concept of start/end tokens.
+- Mention /initialSync to get token.
-- Aliases
+
+Rooms
+=====
+- How are they created? PDU anchor point: "root of the tree".
+- Adding / removing aliases.
- Invite/join dance
- State and non-state data (+extensibility)
@@ -46,10 +156,8 @@ TODO : Room permissions / config / power levels.
Messages
========
-This specification outlines several standard message types, all of which are
-prefixed with "m.".
-
-- Namespacing?
+This specification outlines several standard event types, all of which are
+prefixed with ``m.``
State messages
--------------
@@ -102,15 +210,15 @@ below:
- ``body`` : "string" - The alt text of the image, or some kind of content
description for accessibility e.g. "image attachment".
-ImageInfo:
- Information about an image::
+ ImageInfo:
+ Information about an image::
- {
- "size" : integer (size of image in bytes),
- "w" : integer (width of image in pixels),
- "h" : integer (height of image in pixels),
- "mimetype" : "string (e.g. image/jpeg)",
- }
+ {
+ "size" : integer (size of image in bytes),
+ "w" : integer (width of image in pixels),
+ "h" : integer (height of image in pixels),
+ "mimetype" : "string (e.g. image/jpeg)",
+ }
``m.audio``
Required keys:
@@ -121,15 +229,14 @@ ImageInfo:
- ``body`` : "string" - A description of the audio e.g. "Bee Gees -
Stayin' Alive", or some kind of content description for accessibility e.g.
"audio attachment".
+ AudioInfo:
+ Information about a piece of audio::
-AudioInfo:
- Information about a piece of audio::
-
- {
- "mimetype" : "string (e.g. audio/aac)",
- "size" : integer (size of audio in bytes),
- "duration" : integer (duration of audio in milliseconds),
- }
+ {
+ "mimetype" : "string (e.g. audio/aac)",
+ "size" : integer (size of audio in bytes),
+ "duration" : integer (duration of audio in milliseconds),
+ }
``m.video``
Required keys:
@@ -140,18 +247,18 @@ AudioInfo:
- ``body`` : "string" - A description of the video e.g. "Gangnam style",
or some kind of content description for accessibility e.g. "video attachment".
-VideoInfo:
- Information about a video::
+ VideoInfo:
+ Information about a video::
- {
- "mimetype" : "string (e.g. video/mp4)",
- "size" : integer (size of video in bytes),
- "duration" : integer (duration of video in milliseconds),
- "w" : integer (width of video in pixels),
- "h" : integer (height of video in pixels),
- "thumbnail_url" : "string (URL to image)",
- "thumbanil_info" : JSON object (ImageInfo)
- }
+ {
+ "mimetype" : "string (e.g. video/mp4)",
+ "size" : integer (size of video in bytes),
+ "duration" : integer (duration of video in milliseconds),
+ "w" : integer (width of video in pixels),
+ "h" : integer (height of video in pixels),
+ "thumbnail_url" : "string (URL to image)",
+ "thumbanil_info" : JSON object (ImageInfo)
+ }
``m.location``
Required keys:
@@ -174,88 +281,59 @@ The following keys can be attached to any ``m.room.message``:
Presence
========
-Each user has the concept of Presence information. This encodes a sense of the
-"availability" of that user, suitable for display on other user's clients.
-
-The basic piece of presence information is an enumeration of a small set of
-state; such as "free to chat", "online", "busy", or "offline". The default state
-unless the user changes it is "online". Lower states suggest some amount of
-decreased availability from normal, which might have some client-side effect
-like muting notification sounds and suggests to other users not to bother them
-unless it is urgent. Equally, the "free to chat" state exists to let the user
-announce their general willingness to receive messages moreso than default.
-
-Home servers should also allow a user to set their state as "hidden" - a state
-which behaves as offline, but allows the user to see the client state anyway and
-generally interact with client features such as reading message history or
-accessing contacts in the address book.
-
-This basic state field applies to the user as a whole, regardless of how many
+Each user has the concept of presence information. This encodes the
+"availability" of that user, suitable for display on other user's clients. This
+is transmitted as an ``m.presence`` event and is one of the few events which
+are sent *outside the context of a room*. The basic piece of presence information
+is represented by the ``state`` key, which is an enum of one of the following:
+
+ - ``online`` : The default state when the user is connected to an event stream.
+ - ``unavailable`` : The user is not reachable at this time.
+ - ``offline`` : The user is not connected to an event stream.
+ - ``free_for_chat`` : The user is generally willing to receive messages
+ moreso than default.
+ - ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
+ state anyway and generally interact with client features.
+
+This basic ``state`` field applies to the user as a whole, regardless of how many
client devices they have connected. The home server should synchronise this
status choice among multiple devices to ensure the user gets a consistent
experience.
Idle Time
---------
-As well as the basic state field, the presence information can also show a sense
+As well as the basic ``state`` field, the presence information can also show a sense
of an "idle timer". This should be maintained individually by the user's
-clients, and the homeserver can take the highest reported time as that to
-report. Likely this should be presented in fairly coarse granularity; possibly
-being limited to letting the home server automatically switch from a "free to
-chat" or "online" mode into "idle".
+clients, and the home server can take the highest reported time as that to
+report. When a user is offline, the home server can still report when the user was last
+seen online.
-When a user is offline, the Home Server can still report when the user was last
-seen online, again perhaps in a somewhat coarse manner.
-
-Device Type
------------
-Client devices that may limit the user experience somewhat (such as "mobile"
-devices with limited ability to type on a real keyboard or read large amounts of
-text) should report this to the home server, as this is also useful information
-to report as "presence" if the user cannot be expected to provide a good typed
-response to messages.
-
-- m.presence and enums (when should they be used)
+Transmission
+------------
+- Transmitted as an EDU.
+- Presence lists determine who to send to.
Presence List
-------------
Each user's home server stores a "presence list" for that user. This stores a
-list of other user IDs the user has chosen to add to it (remembering any ACL
-Pointer if appropriate).
-
-To be added to a contact list, the user being added must grant permission. Once
-granted, both user's HS(es) store this information, as it allows the user who
-has added the contact some more abilities; see below. Since such subscriptions
+list of other user IDs the user has chosen to add to it. To be added to this
+list, the user being added must receive permission from the list owner. Once
+granted, both user's HS(es) store this information. Since such subscriptions
are likely to be bidirectional, HSes may wish to automatically accept requests
when a reverse subscription already exists.
-As a convenience, presence lists should support the ability to collect users
-into groups, which could allow things like inviting the entire group to a new
-("ad-hoc") chat room, or easy interaction with the profile information ACL
-implementation of the HS.
-
Presence and Permissions
------------------------
For a viewing user to be allowed to see the presence information of a target
-user, either
+user, either:
- * The target user has allowed the viewing user to add them to their presence
+ - The target user has allowed the viewing user to add them to their presence
list, or
-
- * The two users share at least one room in common
+ - The two users share at least one room in common
In the latter case, this allows for clients to display some minimal sense of
presence information in a user list for a room.
-Home servers can also use the user's choice of presence state as a signal for
-how to handle new private one-to-one chat message requests. For example, it
-might decide:
-
- - "free to chat": accept anything
- - "online": accept from anyone in my address book list
- - "busy": accept from anyone in this "important people" group in my address
- book list
-
Typing notifications
====================
@@ -274,18 +352,14 @@ human-friendly string. 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.
-It is also conceivable that since we are attempting to provide a
-worldwide-applicable messaging system, that users may wish to present different
-subsets of information in their profile to different other people, from a
-privacy and permissions perspective.
-
A Profile consists of a display name, an avatar picture, and a set of other
metadata fields that the user may wish to publish (email address, phone
numbers, website URLs, etc...). This specification puts no requirements on the
-display name other than it being a valid Unicode string.
+display name other than it being a valid unicode string.
- Metadata extensibility
- Bundled with which events? e.g. m.room.member
+- Generate own events? What type?
Registration and login
======================
@@ -312,8 +386,8 @@ The login process breaks down into the following:
step 2.
As each home server may have different ways of logging in, the client needs to know how
-they should login. All distinct login stages MUST have a corresponding ``'type'``.
-A ``'type'`` is a namespaced string which details the mechanism for logging in.
+they should login. All distinct login stages MUST have a corresponding ``type``.
+A ``type`` is a namespaced string which details the mechanism for logging in.
A client may be able to login via multiple valid login flows, and should choose a single
flow when logging in. A flow is a series of login stages. The home server MUST respond
@@ -359,17 +433,17 @@ subsequent requests until the login is completed::
}
This specification defines the following login types:
- - m.login.password
- - m.login.oauth2
- - m.login.email.code
- - m.login.email.url
+ - ``m.login.password``
+ - ``m.login.oauth2``
+ - ``m.login.email.code``
+ - ``m.login.email.url``
Password-based
--------------
-Type:
- "m.login.password"
-Description:
+:Type:
+ m.login.password
+:Description:
Login is supported via a username and password.
To respond to this type, reply with::
@@ -385,9 +459,9 @@ process, or a standard error response.
OAuth2-based
------------
-Type:
- "m.login.oauth2"
-Description:
+:Type:
+ m.login.oauth2
+:Description:
Login is supported via OAuth2 URLs. This login consists of multiple requests.
To respond to this type, reply with::
@@ -438,9 +512,9 @@ visits the REDIRECT_URI with the auth code= query parameter which returns::
Email-based (code)
------------------
-Type:
- "m.login.email.code"
-Description:
+:Type:
+ m.login.email.code
+:Description:
Login is supported by typing in a code which is sent in an email. This login
consists of multiple requests.
@@ -473,9 +547,9 @@ the login process, or a standard error response.
Email-based (url)
-----------------
-Type:
- "m.login.email.url"
-Description:
+:Type:
+ m.login.email.url
+:Description:
Login is supported by clicking on a URL in an email. This login consists of
multiple requests.
@@ -515,7 +589,7 @@ N-Factor Authentication
-----------------------
Multiple login stages can be combined to create N-factor authentication during login.
-This can be achieved by responding with the ``'next'`` login type on completion of a
+This can be achieved by responding with the ``next`` login type on completion of a
previous login stage::
{
@@ -523,7 +597,7 @@ previous login stage::
}
If a home server implements N-factor authentication, it MUST respond with all
-``'stages'`` when initially queried for their login requirements::
+``stages`` when initially queried for their login requirements::
{
"type": "<1st login type>",
@@ -592,59 +666,62 @@ can also be performed.
There are three main kinds of communication that occur between home servers:
- * Queries
+:Queries:
These are single request/response interactions between a given pair of
- servers, initiated by one side sending an HTTP request to obtain some
+ servers, initiated by one side sending an HTTP 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.
- * EDUs - Ephemeral Data Units
+:Ephemeral Data Units (EDUs):
These are notifications of events that are pushed from one home server to
another. They are not persisted and contain no long-term significant history,
nor does the receiving home server have to reply to them.
- * PDUs - Persisted Data Units
+:Persisted Data Units (PDUs):
These are notifications of events that are broadcast from one home server to
any others that are interested in the same "context" (namely, a Room ID).
They are persisted to long-term storage and form the record of history for
that context.
-Where Queries are presented directly across the HTTP connection as GET requests
-to specific URLs, EDUs and PDUs are further wrapped in an envelope called a
-Transaction, which is transferred from the origin to the destination home server
-using a PUT request.
+EDUs and PDUs are further wrapped in an envelope called a Transaction, which is
+transferred from the origin to the destination home server using an HTTP PUT request.
-Transactions and EDUs/PDUs
---------------------------
+Transactions
+------------
The transfer of EDUs and PDUs between home servers is performed by an exchange
-of Transaction messages, which are encoded as JSON objects with a dict as the
-top-level element, passed over an HTTP PUT request. A Transaction is meaningful
-only to the pair of home servers that exchanged it; they are not globally-
-meaningful.
+of Transaction messages, which are encoded as JSON objects, passed over an
+HTTP PUT request. A Transaction is meaningful only to the pair of home servers that
+exchanged it; they are not globally-meaningful.
-Each transaction has an opaque ID and timestamp (UNIX epoch time in
-milliseconds) generated by its origin server, an origin and destination server
-name, a list of "previous IDs", and a list of PDUs - the actual message payload
-that the Transaction carries.
+Each transaction has:
+ - An opaque transaction ID.
+ - A timestamp (UNIX epoch time in milliseconds) generated by its origin server.
+ - An origin and destination server name.
+ - A list of "previous IDs".
+ - A list of PDUs and EDUs - the actual message payload that the Transaction carries.
- {"transaction_id":"916d630ea616342b42e98a3be0b74113",
+::
+
+ {
+ "transaction_id":"916d630ea616342b42e98a3be0b74113",
"ts":1404835423000,
"origin":"red",
"destination":"blue",
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
"pdus":[...],
- "edus":[...]}
+ "edus":[...]
+ }
-The "previous IDs" field will contain a list of previous transaction IDs that
-the origin server has sent to this destination. Its purpose is to act as a
+The ``prev_ids`` field contains a list of previous transaction IDs that
+the ``origin`` server has sent to this ``destination``. Its purpose is to act as a
sequence checking mechanism - the destination server can check whether it has
successfully received that Transaction, or ask for a retransmission if not.
-The "pdus" field of a transaction is a list, containing zero or more PDUs.[*]
-Each PDU is itself a dict containing a number of keys, the exact details of
-which will vary depending on the type of PDU. Similarly, the "edus" field is
+The ``pdus`` field of a transaction is a list, containing zero or more PDUs.[*]
+Each PDU is itself a JSON object containing a number of keys, the exact details of
+which will vary depending on the type of PDU. Similarly, the ``edus`` field is
another list containing the EDUs. This key may be entirely absent if there are
no EDUs to transfer.
@@ -653,25 +730,35 @@ receiving an "empty" transaction, as this is useful for informing peers of other
transaction IDs they should be aware of. This effectively acts as a push
mechanism to encourage peers to continue to replicate content.)
-All PDUs have an ID, a context, a declaration of their type, a list of other PDU
-IDs that have been seen recently on that context (regardless of which origin
-sent them), and a nested content field containing the actual event content.
+PDUs and EDUs
+-------------
+
+All PDUs have:
+ - An ID
+ - A context
+ - A declaration of their type
+ - A list of other PDU IDs that have been seen recently on that context (regardless of which origin
+ sent them)
[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
[origin,ref] pair like the prev_pdus are]]
- {"pdu_id":"a4ecee13e2accdadf56c1025af232176",
+::
+
+ {
+ "pdu_id":"a4ecee13e2accdadf56c1025af232176",
"context":"#example.green",
"origin":"green",
"ts":1404838188000,
"pdu_type":"m.text",
"prev_pdus":[["blue","99d16afbc857975916f1d73e49e52b65"]],
"content":...
- "is_state":false}
+ "is_state":false
+ }
-In contrast to the transaction layer, it is important to note that the prev_pdus
+In contrast to Transactions, it is important to note that the ``prev_pdus``
field of a PDU refers to PDUs that any origin server has sent, rather than
-previous IDs that this origin has sent. This list may refer to other PDUs sent
+previous IDs that this ``origin`` has sent. This list may refer to other PDUs sent
by the same origin as the current one, or other origins.
Because of the distributed nature of participants in a Matrix conversation, it
@@ -686,6 +773,8 @@ PDUs fall into two main categories: those that deliver Events, and those that
synchronise State. For PDUs that relate to State synchronisation, additional
keys exist to support this:
+::
+
{...,
"is_state":true,
"state_key":TODO
@@ -704,6 +793,8 @@ EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
"previous" IDs. The only mandatory fields for these are the type, origin and
destination home server names, and the actual nested content.
+::
+
{"edu_type":"m.presence",
"origin":"blue",
"destination":"orange",
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 1cc820fb5b..eac110419c 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -146,7 +146,7 @@ class FederationHandler(BaseHandler):
# Huh, let's try and get the current state
try:
yield self.replication_layer.get_state_for_context(
- origin, event.room_id
+ event.origin, event.room_id
)
hosts = yield self.store.get_joined_hosts_for_room(
diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py
index 4f42afc015..4945b0796e 100644
--- a/synapse/storage/stream.py
+++ b/synapse/storage/stream.py
@@ -294,7 +294,7 @@ class StreamStore(SQLBaseStore):
logger.debug("get_room_events_max_id: %s", res)
if not res or not res[0] or not res[0]["m"]:
- return "s1"
+ return "s0"
key = res[0]["m"]
return "s%d" % (key,)
diff --git a/webclient/app.js b/webclient/app.js
index 2d6624ceec..02695c3ae6 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -24,6 +24,8 @@ var matrixWebClient = angular.module('matrixWebClient', [
'SettingsController',
'UserController',
'matrixService',
+ 'matrixPhoneService',
+ 'MatrixCall',
'eventStreamService',
'eventHandlerService',
'infinite-scroll'
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index df61429db5..2f7580d682 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -95,7 +95,6 @@ angular.module('eventHandlerService', [])
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
};
-
return {
MSG_EVENT: MSG_EVENT,
MEMBER_EVENT: MEMBER_EVENT,
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 4cc2bf4c4e..441148670e 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -25,7 +25,8 @@ the eventHandlerService.
angular.module('eventStreamService', [])
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
var END = "END";
- var TIMEOUT_MS = 30000;
+ var SERVER_TIMEOUT_MS = 30000;
+ var CLIENT_TIMEOUT_MS = 40000;
var ERR_TIMEOUT_MS = 5000;
var settings = {
@@ -55,7 +56,7 @@ angular.module('eventStreamService', [])
deferred = deferred || $q.defer();
// run the stream from the latest token
- matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
+ matrixService.getEventStream(settings.from, SERVER_TIMEOUT_MS, CLIENT_TIMEOUT_MS).then(
function(response) {
if (!settings.isActive) {
console.log("[EventStream] Got response but now inactive. Dropping data.");
@@ -80,7 +81,7 @@ angular.module('eventStreamService', [])
}
},
function(error) {
- if (error.status == 403) {
+ if (error.status === 403) {
settings.shouldPoll = false;
}
diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js
new file mode 100644
index 0000000000..3aab6413fc
--- /dev/null
+++ b/webclient/components/matrix/matrix-call.js
@@ -0,0 +1,264 @@
+/*
+Copyright 2014 matrix.org
+
+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';
+
+var forAllVideoTracksOnStream = function(s, f) {
+ var tracks = s.getVideoTracks();
+ for (var i = 0; i < tracks.length; i++) {
+ f(tracks[i]);
+ }
+}
+
+var forAllAudioTracksOnStream = function(s, f) {
+ var tracks = s.getAudioTracks();
+ for (var i = 0; i < tracks.length; i++) {
+ f(tracks[i]);
+ }
+}
+
+var forAllTracksOnStream = function(s, f) {
+ forAllVideoTracksOnStream(s, f);
+ forAllAudioTracksOnStream(s, f);
+}
+
+angular.module('MatrixCall', [])
+.factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) {
+ var MatrixCall = function(room_id) {
+ this.room_id = room_id;
+ this.call_id = "c" + new Date().getTime();
+ this.state = 'fledgling';
+ }
+
+ navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+
+ window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
+
+ MatrixCall.prototype.placeCall = function() {
+ self = this;
+ matrixPhoneService.callPlaced(this);
+ navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); });
+ self.state = 'wait_local_media';
+ };
+
+ MatrixCall.prototype.initWithInvite = function(msg) {
+ this.msg = msg;
+ this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]})
+ self= this;
+ this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
+ this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
+ this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
+ this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
+ this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
+ this.state = 'ringing';
+ };
+
+ MatrixCall.prototype.answer = function() {
+ console.trace("Answering call "+this.call_id);
+ self = this;
+ navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); });
+ this.state = 'wait_local_media';
+ };
+
+ MatrixCall.prototype.hangup = function() {
+ console.trace("Ending call "+this.call_id);
+
+ forAllTracksOnStream(this.localAVStream, function(t) {
+ t.stop();
+ });
+ forAllTracksOnStream(this.remoteAVStream, function(t) {
+ t.stop();
+ });
+
+ var content = {
+ msgtype: "m.call.hangup",
+ version: 0,
+ call_id: this.call_id,
+ };
+ matrixService.sendMessage(this.room_id, undefined, content).then(this.messageSent, this.messageSendFailed);
+ this.state = 'ended';
+ };
+
+ MatrixCall.prototype.gotUserMediaForInvite = function(stream) {
+ this.localAVStream = stream;
+ var audioTracks = stream.getAudioTracks();
+ for (var i = 0; i < audioTracks.length; i++) {
+ audioTracks[i].enabled = true;
+ }
+ this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]})
+ self = this;
+ this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
+ this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
+ this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
+ this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
+ this.peerConn.addStream(stream);
+ this.peerConn.createOffer(function(d) {
+ self.gotLocalOffer(d);
+ }, function(e) {
+ self.getLocalOfferFailed(e);
+ });
+ this.state = 'create_offer';
+ };
+
+ MatrixCall.prototype.gotUserMediaForAnswer = function(stream) {
+ this.localAVStream = stream;
+ var audioTracks = stream.getAudioTracks();
+ for (var i = 0; i < audioTracks.length; i++) {
+ audioTracks[i].enabled = true;
+ }
+ this.peerConn.addStream(stream);
+ self = this;
+ var constraints = {
+ 'mandatory': {
+ 'OfferToReceiveAudio': true,
+ 'OfferToReceiveVideo': false
+ },
+ };
+ this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints);
+ this.state = 'create_answer';
+ };
+
+ MatrixCall.prototype.gotLocalIceCandidate = function(event) {
+ console.trace(event);
+ if (event.candidate) {
+ var content = {
+ msgtype: "m.call.candidate",
+ version: 0,
+ call_id: this.call_id,
+ candidate: event.candidate
+ };
+ matrixService.sendMessage(this.room_id, undefined, content).then(this.messageSent, this.messageSendFailed);
+ }
+ }
+
+ MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
+ console.trace("Got ICE candidate from remote: "+cand);
+ var candidateObject = new RTCIceCandidate({
+ sdpMLineIndex: cand.label,
+ candidate: cand.candidate
+ });
+ this.peerConn.addIceCandidate(candidateObject, function() {}, function(e) {});
+ };
+
+ MatrixCall.prototype.receivedAnswer = function(msg) {
+ this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
+ this.state = 'connecting';
+ };
+
+ MatrixCall.prototype.gotLocalOffer = function(description) {
+ console.trace("Created offer: "+description);
+ this.peerConn.setLocalDescription(description);
+
+ var content = {
+ msgtype: "m.call.invite",
+ version: 0,
+ call_id: this.call_id,
+ offer: description
+ };
+ matrixService.sendMessage(this.room_id, undefined, content).then(this.messageSent, this.messageSendFailed);
+ this.state = 'invite_sent';
+ };
+
+ MatrixCall.prototype.createdAnswer = function(description) {
+ console.trace("Created answer: "+description);
+ this.peerConn.setLocalDescription(description);
+ var content = {
+ msgtype: "m.call.answer",
+ version: 0,
+ call_id: this.call_id,
+ answer: description
+ };
+ matrixService.sendMessage(this.room_id, undefined, content).then(this.messageSent, this.messageSendFailed);
+ this.state = 'connecting';
+ };
+
+ MatrixCall.prototype.messageSent = function() {
+ };
+
+ MatrixCall.prototype.messageSendFailed = function(error) {
+ };
+
+ MatrixCall.prototype.getLocalOfferFailed = function(error) {
+ this.onError("Failed to start audio for call!");
+ };
+
+ MatrixCall.prototype.getUserMediaFailed = function() {
+ this.onError("Couldn't start capturing audio! Is your microphone set up?");
+ };
+
+ MatrixCall.prototype.onIceConnectionStateChanged = function() {
+ console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState);
+ // ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
+ if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
+ this.state = 'connected';
+ }
+ };
+
+ MatrixCall.prototype.onSignallingStateChanged = function() {
+ console.trace("Signalling state changed to: "+this.peerConn.signalingState);
+ };
+
+ MatrixCall.prototype.onSetRemoteDescriptionSuccess = function() {
+ console.trace("Set remote description");
+ };
+
+ MatrixCall.prototype.onSetRemoteDescriptionError = function(e) {
+ console.trace("Failed to set remote description"+e);
+ };
+
+ MatrixCall.prototype.onAddStream = function(event) {
+ console.trace("Stream added"+event);
+
+ var s = event.stream;
+
+ this.remoteAVStream = s;
+
+ var self = this;
+ forAllTracksOnStream(s, function(t) {
+ // not currently implemented in chrome
+ t.onstarted = self.onRemoteStreamTrackStarted;
+ });
+
+ // not currently implemented in chrome
+ event.stream.onstarted = this.onRemoteStreamStarted;
+ var player = new Audio();
+ player.src = URL.createObjectURL(s);
+ player.play();
+ };
+
+ MatrixCall.prototype.onRemoteStreamStarted = function(event) {
+ this.state = 'connected';
+ };
+
+ MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) {
+ this.state = 'connected';
+ };
+
+ MatrixCall.prototype.onHangupReceived = function() {
+ this.state = 'ended';
+
+ forAllTracksOnStream(this.localAVStream, function(t) {
+ t.stop();
+ });
+ forAllTracksOnStream(this.remoteAVStream, function(t) {
+ t.stop();
+ });
+
+ this.onHangup();
+ };
+
+ return MatrixCall;
+}]);
diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js
new file mode 100644
index 0000000000..7f1ff531c4
--- /dev/null
+++ b/webclient/components/matrix/matrix-phone-service.js
@@ -0,0 +1,68 @@
+/*
+Copyright 2014 matrix.org
+
+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('matrixPhoneService', [])
+.factory('matrixPhoneService', ['$rootScope', '$injector', 'matrixService', 'eventHandlerService', function MatrixPhoneService($rootScope, $injector, matrixService, eventHandlerService) {
+ var matrixPhoneService = function() {
+ };
+
+ matrixPhoneService.CALL_EVENT = "CALL_EVENT";
+ matrixPhoneService.allCalls = {};
+
+ matrixPhoneService.callPlaced = function(call) {
+ matrixPhoneService.allCalls[call.call_id] = call;
+ };
+
+ $rootScope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
+ if (!isLive) return; // until matrix supports expiring messages
+ if (event.user_id == matrixService.config().user_id) return;
+ var msg = event.content;
+ if (msg.msgtype == 'm.call.invite') {
+ var MatrixCall = $injector.get('MatrixCall');
+ var call = new MatrixCall(event.room_id);
+ call.call_id = msg.call_id;
+ call.initWithInvite(msg);
+ matrixPhoneService.allCalls[call.call_id] = call;
+ $rootScope.$broadcast(matrixPhoneService.CALL_EVENT, call);
+ } else if (msg.msgtype == 'm.call.answer') {
+ var call = matrixPhoneService.allCalls[msg.call_id];
+ if (!call) {
+ console.trace("Got answer for unknown call ID "+msg.call_id);
+ return;
+ }
+ call.receivedAnswer(msg);
+ } else if (msg.msgtype == 'm.call.candidate') {
+ var call = matrixPhoneService.allCalls[msg.call_id];
+ if (!call) {
+ console.trace("Got candidate for unknown call ID "+msg.call_id);
+ return;
+ }
+ call.gotRemoteIceCandidate(msg.candidate);
+ } else if (msg.msgtype == 'm.call.hangup') {
+ var call = matrixPhoneService.allCalls[msg.call_id];
+ if (!call) {
+ console.trace("Got hangup for unknown call ID "+msg.call_id);
+ return;
+ }
+ call.onHangupReceived();
+ matrixPhoneService.allCalls[msg.call_id] = undefined;
+ }
+ });
+
+ return matrixPhoneService;
+}]);
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index 2feddac5d8..b56eef6af5 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -41,7 +41,7 @@ angular.module('matrixService', [])
var prefixPath = "/matrix/client/api/v1";
var MAPPING_PREFIX = "alias_for_";
- var doRequest = function(method, path, params, data) {
+ var doRequest = function(method, path, params, data, $httpParams) {
if (!config) {
console.warn("No config exists. Cannot perform request to "+path);
return;
@@ -58,7 +58,7 @@ angular.module('matrixService', [])
path = prefixPath + path;
}
- return doBaseRequest(config.homeserver, method, path, params, data, undefined);
+ return doBaseRequest(config.homeserver, method, path, params, data, undefined, $httpParams);
};
var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
@@ -343,15 +343,31 @@ angular.module('matrixService', [])
return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
},
-
- // start listening on /events
- getEventStream: function(from, timeout) {
+
+ /**
+ * Start listening on /events
+ * @param {String} from the token from which to listen events to
+ * @param {Integer} serverTimeout the time in ms the server will hold open the connection
+ * @param {Integer} clientTimeout the timeout in ms used at the client HTTP request level
+ * @returns a promise
+ */
+ getEventStream: function(from, serverTimeout, clientTimeout) {
var path = "/events";
var params = {
from: from,
- timeout: timeout
+ timeout: serverTimeout
};
- return doRequest("GET", path, params);
+
+ var $httpParams;
+ if (clientTimeout) {
+ // If the Internet connection is lost, this timeout is used to be able to
+ // cancel the current request and notify the client so that it can retry with a new request.
+ $httpParams = {
+ timeout: clientTimeout
+ };
+ }
+
+ return doRequest("GET", path, params, undefined, $httpParams);
},
// Indicates if user authentications details are stored in cache
@@ -420,34 +436,38 @@ angular.module('matrixService', [])
/****** Room aliases management ******/
/**
- * Enhance data returned by rooms() and publicRooms() by adding room_alias
- * & room_display_name which are computed from data already retrieved from the server.
- * @param {Array} data the response of rooms() and publicRooms()
- * @returns {Array} the same array with enriched objects
+ * Get the room_alias & room_display_name which are computed from data
+ * already retrieved from the server.
+ * @param {Room object} room one element of the array returned by the response
+ * of rooms() and publicRooms()
+ * @returns {Object} {room_alias: "...", room_display_name: "..."}
*/
- assignRoomAliases: function(data) {
- for (var i=0; i<data.length; i++) {
- var alias = this.getRoomIdToAliasMapping(data[i].room_id);
- if (alias) {
- // use the existing alias from storage
- data[i].room_alias = alias;
- data[i].room_display_name = alias;
- }
- else if (data[i].aliases && data[i].aliases[0]) {
- // save the mapping
- // TODO: select the smarter alias from the array
- this.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
- data[i].room_display_name = data[i].aliases[0];
- }
- else if (data[i].membership == "invite" && "inviter" in data[i]) {
- data[i].room_display_name = data[i].inviter + "'s room"
- }
- else {
- // last resort use the room id
- data[i].room_display_name = data[i].room_id;
- }
+ getRoomAliasAndDisplayName: function(room) {
+ var result = {
+ room_alias: undefined,
+ room_display_name: undefined
+ };
+
+ var alias = this.getRoomIdToAliasMapping(room.room_id);
+ if (alias) {
+ // use the existing alias from storage
+ result.room_alias = alias;
+ result.room_display_name = alias;
+ }
+ else if (room.aliases && room.aliases[0]) {
+ // save the mapping
+ // TODO: select the smarter alias from the array
+ this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]);
+ result.room_display_name = room.aliases[0];
+ }
+ else if (room.membership === "invite" && "inviter" in room) {
+ result.room_display_name = room.inviter + "'s room";
+ }
+ else {
+ // last resort use the room id
+ result.room_display_name = room.room_id;
}
- return data;
+ return result;
},
createRoomIdToAliasMapping: function(roomId, alias) {
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
index 008dff7422..547a5c5603 100644
--- a/webclient/home/home-controller.js
+++ b/webclient/home/home-controller.js
@@ -42,7 +42,13 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
matrixService.publicRooms().then(
function(response) {
- $scope.public_rooms = matrixService.assignRoomAliases(response.data.chunk);
+ $scope.public_rooms = response.data.chunk;
+ for (var i = 0; i < $scope.public_rooms.length; i++) {
+ var room = $scope.public_rooms[i];
+
+ // Add room_alias & room_display_name members
+ angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
+ }
}
);
};
diff --git a/webclient/index.html b/webclient/index.html
index 16f0e8ac5f..5faf165626 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -26,6 +26,8 @@
<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-call.js"></script>
+ <script src="components/matrix/matrix-phone-service.js"></script>
<script src="components/matrix/event-stream-service.js"></script>
<script src="components/matrix/event-handler-service.js"></script>
<script src="components/matrix/presence-service.js"></script>
diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js
index 803ab420f9..d33d41a922 100644
--- a/webclient/recents/recents-controller.js
+++ b/webclient/recents/recents-controller.js
@@ -29,7 +29,7 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
// Refresh the list on matrix invitation and message event
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
var config = matrixService.config();
- if (event.state_key === config.user_id && event.content.membership === "invite") {
+ 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;
@@ -39,7 +39,9 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
}
});
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
- $scope.rooms[event.room_id].lastMsg = event;
+ if (isLive) {
+ $scope.rooms[event.room_id].lastMsg = event;
+ }
});
};
@@ -53,13 +55,16 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
// Reset data
$scope.rooms = {};
- var data = matrixService.assignRoomAliases(response.data.rooms);
- for (var i=0; i<data.length; i++) {
- $scope.rooms[data[i].room_id] = data[i];
+ var rooms = response.data.rooms;
+ for (var i=0; i<rooms.length; i++) {
+ var room = rooms[i];
+
+ // Add room_alias & room_display_name members
+ $scope.rooms[room.room_id] = angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
// Create a shortcut for the last message of this room
- if (data[i].messages && data[i].messages.chunk && data[i].messages.chunk[0]) {
- $scope.rooms[data[i].room_id].lastMsg = data[i].messages.chunk[0];
+ if (room.messages && room.messages.chunk && room.messages.chunk[0]) {
+ $scope.rooms[room.room_id].lastMsg = room.messages.chunk[0];
}
}
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index 6fda6c5c6b..3f025a98d8 100644
--- a/webclient/recents/recents.html
+++ b/webclient/recents/recents.html
@@ -39,6 +39,11 @@
{{ room.lastMsg.user_id }} sent an image
</div>
+ <div ng-switch-when="m.emote">
+ <span ng-bind-html="'* ' + (room.lastMsg.user_id) + ' ' + room.lastMsg.content.body | linky:'_blank'">
+ </span>
+ </div>
+
<div ng-switch-default>
{{ room.lastMsg.content }}
</div>
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 910168754c..15710d2ba3 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
-.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
- function($scope, $http, $timeout, $routeParams, $location, matrixService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
+angular.module('RoomController', ['ngSanitize', 'mFileInput'])
+.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
+ function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
'use strict';
var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320;
@@ -82,6 +82,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
$scope.$on(eventHandlerService.PRESENCE_EVENT, function(ngEvent, event, isLive) {
updatePresence(event);
});
+
+ $rootScope.$on(matrixPhoneService.CALL_EVENT, function(ngEvent, call) {
+ console.trace("incoming call");
+ call.onError = $scope.onCallError;
+ call.onHangup = $scope.onCallHangup;
+ $scope.currentCall = call;
+ });
$scope.paginateMore = function() {
if ($scope.state.can_paginate) {
@@ -89,6 +96,15 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
paginate(MESSAGES_PER_PAGINATION);
}
};
+
+ $scope.answerCall = function() {
+ $scope.currentCall.answer();
+ };
+
+ $scope.hangupCall = function() {
+ $scope.currentCall.hangup();
+ $scope.currentCall = undefined;
+ };
var paginate = function(numItems) {
// console.log("paginate " + numItems);
@@ -454,4 +470,21 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
$scope.loadMoreHistory = function() {
paginate(MESSAGES_PER_PAGINATION);
};
+
+ $scope.startVoiceCall = function() {
+ var call = new MatrixCall($scope.room_id);
+ call.onError = $scope.onCallError;
+ call.onHangup = $scope.onCallHangup;
+ call.placeCall();
+ $scope.currentCall = call;
+ }
+
+ $scope.onCallError = function(errStr) {
+ $scope.feedback = errStr;
+ }
+
+ $scope.onCallHangup = function() {
+ $scope.feedback = "Call ended";
+ $scope.currentCall = undefined;
+ }
}]);
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 236ca0a89b..572c52e64e 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -45,13 +45,13 @@
</td>
<td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
<div class="bubble">
- <span ng-hide='msg.type !== "m.room.member"'>
+ <span ng-show='msg.type === "m.room.member"'>
{{ members[msg.user_id].displayname || msg.user_id }}
{{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
{{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }}
</span>
- <span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
- <span ng-hide='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.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'"/>
<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 }}"/>
@@ -98,10 +98,18 @@
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
</span>
<button ng-click="leaveRoom()">Leave</button>
+ <button ng-click="startVoiceCall()" ng-show="currentCall == undefined">Voice Call</button>
+ <div ng-show="currentCall.state == 'ringing'">
+ Incoming call from {{ currentCall.user_id }}
+ <button ng-click="answerCall()">Answer</button>
+ <button ng-click="hangupCall()">Reject</button>
+ </div>
+ <button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing'">Hang up</button>
+ {{ currentCall.state }}
</div>
{{ feedback }}
- <div ng-hide="!state.stream_failure">
+ <div ng-show="state.stream_failure">
{{ state.stream_failure.data.error || "Connection failure" }}
</div>
</div>
|