Merge branch 'develop' into server2server_tls

This commit is contained in:
Mark Haines 2014-09-01 13:29:17 +01:00
commit a9512d0994
60 changed files with 1298 additions and 754 deletions

7
WISHLIST.rst Normal file
View File

@ -0,0 +1,7 @@
Broad-sweeping stuff which would be nice to have
================================================
- Additional SQL backends beyond sqlite
- homeserver implementation in go
- homeserver implementation in node.js
- client SDKs

View File

@ -60,7 +60,7 @@ class SynapseCmd(cmd.Cmd):
"complete_usernames": "on",
"send_delivery_receipts": "on"
}
self.path_prefix = "/matrix/client/api/v1"
self.path_prefix = "/_matrix/client/api/v1"
self.event_stream_token = "END"
self.prompt = ">>> "
@ -252,7 +252,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks
def _do_emailrequest(self, args):
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/requestToken"
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']})
@ -274,7 +274,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks
def _do_emailvalidate(self, args):
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/submitToken"
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']})
@ -294,7 +294,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks
def _do_3pidbind(self, args):
url = self._identityServerUrl()+"/matrix/identity/api/v1/3pid/bind"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/3pid/bind"
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']})
@ -360,14 +360,14 @@ class SynapseCmd(cmd.Cmd):
def _do_invite(self, roomid, userstring):
if (not userstring.startswith('@') and
self._is_on("complete_usernames")):
url = self._identityServerUrl()+"/matrix/identity/api/v1/lookup"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/lookup"
json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring})
mxid = None
if 'mxid' in json_res and 'signatures' in json_res:
url = self._identityServerUrl()+"/matrix/identity/api/v1/pubkey/ed25519"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/pubkey/ed25519"
pubKey = None
pubKeyObj = yield self.http_client.do_request("GET", url)

View File

@ -30,7 +30,7 @@ Registration
The aim of registration is to get a user ID and access token which you will need
when accessing other APIs::
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/register"
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/_matrix/client/api/v1/register"
{
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",
@ -51,13 +51,13 @@ Login
-----
The aim when logging in is to get an access token for your existing user ID::
curl -XGET "http://localhost:8080/matrix/client/api/v1/login"
curl -XGET "http://localhost:8080/_matrix/client/api/v1/login"
{
"type": "m.login.password"
}
curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/login"
curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/_matrix/client/api/v1/login"
{
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",
@ -87,7 +87,7 @@ Creating a room
If you want to send a message to someone, you have to be in a room with them. To
create a room::
curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/_matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
{
"room_alias": "#tutorial:localhost",
@ -105,7 +105,7 @@ Sending messages
----------------
You can now send messages to this room::
curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
NB: There are no limitations to the types of messages which can be exchanged.
The only requirement is that ``"msgtype"`` is specified.
@ -127,7 +127,7 @@ Inviting a user to a room
-------------------------
You can directly invite a user to a room like so::
curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
This informs ``@myfriend:localhost`` of the room ID
``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room.
@ -137,7 +137,7 @@ Joining a room via an invite
If you receive an invite, you can join the room by changing the membership to
join::
curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
NB: Only the person invited (``@myfriend:localhost``) can change the membership
state to ``"join"``.
@ -147,7 +147,7 @@ Joining a room via an alias
Alternatively, if you know the room alias for this room and the room config
allows it, you can directly join a room via the alias::
curl -XPUT -d '{}' "http://localhost:8080/matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
curl -XPUT -d '{}' "http://localhost:8080/_matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
{
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
@ -173,7 +173,7 @@ Getting all state
If the client doesn't know any information on the rooms the user is
invited/joined on, they can get all the user's state for all rooms::
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
curl -XGET "http://localhost:8080/_matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
[
{
@ -236,7 +236,7 @@ all of the messages and feedback for these rooms. This can be a LOT of data. You
may just want the most recent message for each room. This can be achieved by
applying pagination stream parameters to this request::
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
curl -XGET "http://localhost:8080/_matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
[
{
@ -271,7 +271,7 @@ Getting live state
Once you know which rooms the client has previously interacted with, you need to
listen for incoming events. This can be done like so::
curl -XGET "http://localhost:8080/matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
curl -XGET "http://localhost:8080/_matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
{
"chunk": [],

View File

@ -306,11 +306,11 @@ POST requests MUST be submitted as application/json.
All paths MUST be namespaced by the version of the API being used. This should
be:
/matrix/client/api/v1
/_matrix/client/api/v1
All REST paths in this section MUST be prefixed with this. E.g.
REST Path: /rooms/$room_id
Absolute Path: /matrix/client/api/v1/rooms/$room_id
Absolute Path: /_matrix/client/api/v1/rooms/$room_id
Registration
============

View File

@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/directory",
"produces": [
"application/json"

View File

@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/events",
"produces": [
"application/json"

View File

@ -40,7 +40,7 @@
"path": "/login"
}
],
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"consumes": [
"application/json"
],

View File

@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/presence",
"produces": [
"application/json"

View File

@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/profile",
"produces": [
"application/json"

View File

@ -37,7 +37,7 @@
"path": "/register"
}
],
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"consumes": [
"application/json"
],

View File

@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/rooms",
"produces": [
"application/json"

View File

@ -155,7 +155,7 @@ Protocol URLs
All these URLs are namespaced within a prefix of
/matrix/federation/v1/...
/_matrix/federation/v1/...
For active pushing of messages representing live activity "as it happens":

View File

@ -35,8 +35,8 @@ 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.
The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user. They are
case-insensitive.
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
@ -60,7 +60,8 @@ identified via a "Room ID", which look like::
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.
domain specified. Room IDs are not meant to be human readable. They ARE
case-sensitive.
The following diagram shows an ``m.room.message`` event being sent in the room
``!qporfwt:matrix.org``::
@ -102,10 +103,10 @@ Each room can also have multiple "Room Aliases", which looks like::
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.
which can be used to publicise rooms. They are case-insensitive. 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.
::
@ -214,24 +215,150 @@ In contrast, these are invalid requests::
"key": "This is a put but it is missing a txnId."
}
- TODO: All strings everywhere are UTF-8
Receiving live updates on a client
----------------------------------
- C-S longpoll event stream
- Concept of start/end tokens.
- Mention /initialSync to get token.
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 the client is authorised
to view will appear in the event stream. When the stream is closed, an ``end`` token is
returned. This token can be used in the next request to continue where the client left off.
When the client first logs in, they will need to initially synchronise with their home
server. This is achieved via the ``/initialSync`` API. This API also returns an ``end``
token which can be used with the event stream.
Rooms
=====
- How are they created? PDU anchor point: "root of the tree".
Creation
--------
To create a room, a client has to use the ``/createRoom`` API. There are various options
which can be set when creating a room:
``visibility``
Type:
String
Optional:
Yes
Value:
Either ``public`` or ``private``.
Description:
A ``public`` visibility indicates that the room will be shown in the public room list. A
``private`` visibility will hide the room from the public room list. Rooms default to
``public`` visibility if this key is not included.
``room_alias_name``
Type:
String
Optional:
Yes
Value:
The room alias localpart.
Description:
If this is included, a room alias will be created and mapped to the newly created room.
The alias will belong on the same home server which created the room, e.g.
``!qadnasoi:domain.com >>> #room_alias_name:domain.com``
Example::
{
"visibility": "public",
"room_alias_name": "the pub"
}
- TODO: This creates a room creation event which serves as the root of the PDU graph for this room.
Modifying aliases
-----------------
- Adding / removing aliases.
- Invite/join dance
- State and non-state data (+extensibility)
TODO : Room permissions / config / power levels.
Permissions
-----------
- TODO : Room permissions / config / power levels. What they are. How do they work. Examples.
Messages
========
Joining rooms
-------------
- What is joining? What permissions / access does it give you? How does this affect /initialSync?
- API to hit (``/join/$alias or id``). Explain how alias joining works (auto-resolving). See "Room events" for more info.
- What does the home server have to do?
- Rooms that DON'T need an invite to join. This follows through onto inviting users section.
- Outline invite join dance?
Inviting users
--------------
- Can invite users to a room if the room config key TODO is set to TODO. Must have required power level.
- Outline invite join dance. What is it? Why is it required? How does it work?
- What does the home server have to do?
The purpose of inviting users to a room is to notify them that the room exists
so they can choose to become a member of that room. Some rooms require that all
users who join a room are previously invited to it (an "invite-only" room).
Whether a given room is an "invite-only" room is determined by the room config
key ``TODO``. It can have one of the following values:
- TODO Room config invite only value explanation
- TODO Room config free-to-join value explanation
Only users who have a membership state of ``join`` in a room can invite new
users to said room. The person being invited must not be in the ``join`` state
in the room. The fully-qualified user ID must be specified when inviting a user,
as the user may reside on a different home server. To invite a user, send the
following request to ``/rooms/<room id>/invite``, which will manage the
entire invitation process::
{
"user_id": "<user id to invite>"
}
Alternatively, the membership state for this user in this room can be modified
directly by sending the following request to
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
{
"membership": "invite"
}
See the "Room events" section for more information on ``m.room.member``.
- TODO: In what circumstances will this NOT be equivalent to ``/invite``?
Leaving rooms
-------------
- API to hit (``$roomid/leave``). See "Room events" for more info.
- Must be joined to leave. How does this affect /initialSync?
- Not ever being in a room is NOT equivalent to have left it (due to membership: leave).
- Need to be re-invited if invite-only room.
- If no more HSes in room, can delete room?
- Is there a dance?
Events in a room
----------------
- Split into state and non-state data
- Explain what they are, semantics, give examples of clobbering / not, use cases (msgs vs room names).
Not too much detail on the actual event contents.
- API to hit.
- Extensibility provided by the API for custom events. Examples.
- How this hooks into ``initialSync``.
- See the "Room Events" section for actual spec on each type.
Syncing a room
--------------
- Single room initial sync. API to hit. Why it might be used (lazy loading)
Getting grouped state events
----------------------------
- ``/members`` and ``/messages`` and the events they return.
- ``/state`` and it returns ALL THE THINGS.
Room Events
===========
This specification outlines several standard event types, all of which are
prefixed with ``m.``
@ -244,7 +371,8 @@ State messages
- m.room.config
- m.room.invite_join
What are they, when are they used, what do they contain, how should they be used
What are they, when are they used, what do they contain, how should they be used.
Link back to explanatory sections (e.g. invite/join/leave sections for m.room.member)
Non-state messages
------------------

View File

@ -120,7 +120,7 @@ def make_graph(pdus, room, filename_prefix):
def get_pdus(host, room):
transaction = json.loads(
urllib2.urlopen(
"http://%s/matrix/federation/v1/context/%s/" % (host, room)
"http://%s/_matrix/federation/v1/context/%s/" % (host, room)
).read()
)

View File

@ -10,7 +10,7 @@ $('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@ -25,7 +25,7 @@ $('.login').live('click', function() {
});
var getCurrentRoomList = function() {
var url = "http://localhost:8080/matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
@ -44,7 +44,7 @@ $('.createRoom').live('click', function() {
data.room_alias_name = roomAlias;
}
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
@ -79,7 +79,7 @@ $('.sendMessage').live('click', function() {
return;
}
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));

View File

@ -7,7 +7,7 @@ var eventStreamInfo = {
var roomInfo = [];
var longpollEventStream = function() {
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$from", eventStreamInfo.from);
@ -48,7 +48,7 @@ $('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@ -65,7 +65,7 @@ $('.login').live('click', function() {
var getCurrentRoomList = function() {
$("#roomId").val("");
var url = "http://localhost:8080/matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
@ -98,7 +98,7 @@ var sendMessage = function(roomId) {
return;
}
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));

View File

@ -10,7 +10,7 @@ var viewingRoomId;
// ************** Event Streaming **************
var longpollEventStream = function() {
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$from", eventStreamInfo.from);
@ -89,7 +89,7 @@ $('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@ -107,7 +107,7 @@ $('.register').live('click', function() {
var user = $("#userReg").val();
var password = $("#passwordReg").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/register",
url: "http://localhost:8080/_matrix/client/api/v1/register",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user_id: user, password: password }),
@ -134,7 +134,7 @@ $('.createRoom').live('click', function() {
data.room_alias_name = roomAlias;
}
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
@ -155,7 +155,7 @@ $('.createRoom').live('click', function() {
// ************** Getting current state **************
var getCurrentRoomList = function() {
var url = "http://localhost:8080/matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
@ -181,7 +181,7 @@ var loadRoomContent = function(roomId) {
var getMessages = function(roomId) {
$("#messages").empty();
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" +
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +
encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
$.getJSON(url, function(data) {
for (var i=data.chunk.length-1; i>=0; --i) {
@ -193,7 +193,7 @@ var getMessages = function(roomId) {
var getMemberList = function(roomId) {
$("#members").empty();
memberInfo = [];
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" +
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +
encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
$.getJSON(url, function(data) {
for (var i=0; i<data.chunk.length; ++i) {
@ -216,7 +216,7 @@ $('.sendMessage').live('click', function() {
var sendMessage = function(roomId, body) {
var msgId = $.now();
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
@ -262,7 +262,7 @@ var setRooms = function(roomList) {
var membership = $(this).find('td:eq(1)').text();
if (membership !== "join") {
console.log("Joining room " + roomId);
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
$.ajax({

View File

@ -11,7 +11,7 @@ $('.register').live('click', function() {
var user = $("#user").val();
var password = $("#password").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/register",
url: "http://localhost:8080/_matrix/client/api/v1/register",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user_id: user, password: password }),
@ -27,7 +27,7 @@ $('.register').live('click', function() {
var login = function(user, password) {
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@ -44,7 +44,7 @@ var login = function(user, password) {
$('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.getJSON("http://localhost:8080/matrix/client/api/v1/login", function(data) {
$.getJSON("http://localhost:8080/_matrix/client/api/v1/login", function(data) {
if (data.flows[0].type !== "m.login.password") {
alert("I don't know how to login with this type: " + data.type);
return;
@ -60,7 +60,7 @@ $('.logout').live('click', function() {
});
$('.testToken').live('click', function() {
var url = "http://localhost:8080/matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
}).fail(function(err) {

View File

@ -18,7 +18,7 @@ $('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@ -39,7 +39,7 @@ var getCurrentRoomList = function() {
// solution but that is out of scope of this fiddle.
$("#rooms").find("tr:gt(0)").remove();
var url = "http://localhost:8080/matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
@ -53,7 +53,7 @@ var getCurrentRoomList = function() {
$('.createRoom').live('click', function() {
var data = {};
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
@ -87,7 +87,7 @@ $('.changeMembership').live('click', function() {
return;
}
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
url = url.replace("$membership", membership);
@ -117,7 +117,7 @@ $('.changeMembership').live('click', function() {
$('.joinAlias').live('click', function() {
var roomAlias = $("#roomAlias").val();
var url = "http://localhost:8080/matrix/client/api/v1/join/$roomalias?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/join/$roomalias?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
$.ajax({

View File

@ -15,7 +15,7 @@
"""Contains the URL paths to prefix various aspects of the server with. """
CLIENT_PREFIX = "/matrix/client/api/v1"
FEDERATION_PREFIX = "/matrix/federation/v1"
WEB_CLIENT_PREFIX = "/matrix/client"
CONTENT_REPO_PREFIX = "/matrix/content"
CLIENT_PREFIX = "/_matrix/client/api/v1"
FEDERATION_PREFIX = "/_matrix/federation/v1"
WEB_CLIENT_PREFIX = "/_matrix/client"
CONTENT_REPO_PREFIX = "/_matrix/content"

View File

@ -274,11 +274,11 @@ class MessageHandler(BaseRoomHandler):
messages, token = yield self.store.get_recent_events_for_room(
event.room_id,
limit=limit,
end_token=now_token.events_key,
end_token=now_token.room_key,
)
start_token = now_token.copy_and_replace("events_key", token[0])
end_token = now_token.copy_and_replace("events_key", token[1])
start_token = now_token.copy_and_replace("room_key", token[0])
end_token = now_token.copy_and_replace("room_key", token[1])
d["messages"] = {
"chunk": [m.get_dict() for m in messages],

View File

@ -260,19 +260,18 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks
def user_joined_room(self, user, room_id):
if user.is_mine:
statuscache = self._get_or_make_usercache(user)
# No actual update but we need to bump the serial anyway for the
# event source
self._user_cachemap_latest_serial += 1
statuscache.update({}, serial=self._user_cachemap_latest_serial)
self.push_update_to_local_and_remote(
observed_user=user,
room_ids=[room_id],
statuscache=self._get_or_offline_usercache(user),
)
else:
self.push_update_to_clients(
observed_user=user,
room_ids=[room_id],
statuscache=self._get_or_offline_usercache(user),
statuscache=statuscache,
)
# We also want to tell them about current presence of people.
@ -722,6 +721,78 @@ class PresenceHandler(BaseHandler):
)
class PresenceEventSource(object):
def __init__(self, hs):
self.hs = hs
self.clock = hs.get_clock()
def get_new_events_for_user(self, user, from_key, limit):
from_key = int(from_key)
presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap
# TODO(paul): limit, and filter by visibility
updates = [(k, cachemap[k]) for k in cachemap
if from_key < cachemap[k].serial]
if updates:
clock = self.clock
latest_serial = max([x[1].serial for x in updates])
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
return ((data, latest_serial))
else:
return (([], presence._user_cachemap_latest_serial))
def get_current_key(self):
presence = self.hs.get_handlers().presence_handler
return presence._user_cachemap_latest_serial
def get_pagination_rows(self, user, pagination_config, key):
# TODO (erikj): Does this make sense? Ordering?
from_token = pagination_config.from_token
to_token = pagination_config.to_token
from_key = int(from_token.presence_key)
if to_token:
to_key = int(to_token.presence_key)
else:
to_key = -1
presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap
# TODO(paul): limit, and filter by visibility
updates = [(k, cachemap[k]) for k in cachemap
if to_key < cachemap[k].serial < from_key]
if updates:
clock = self.clock
earliest_serial = max([x[1].serial for x in updates])
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
if to_token:
next_token = to_token
else:
next_token = from_token
next_token = next_token.copy_and_replace(
"presence_key", earliest_serial
)
return ((data, next_token))
else:
if not to_token:
to_token = from_token.copy_and_replace(
"presence_key", 0
)
return (([], to_token))
class UserPresenceCache(object):
"""Store an observed user's state and status message.

View File

@ -462,3 +462,49 @@ class RoomListHandler(BaseRoomHandler):
chunk = yield self.store.get_rooms(is_public=True)
# FIXME (erikj): START is no longer a valid value
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
class RoomEventSource(object):
def __init__(self, hs):
self.store = hs.get_datastore()
@defer.inlineCallbacks
def get_new_events_for_user(self, user, from_key, limit):
# We just ignore the key for now.
to_key = yield self.get_current_key()
events, end_key = yield self.store.get_room_events_stream(
user_id=user.to_string(),
from_key=from_key,
to_key=to_key,
room_id=None,
limit=limit,
)
defer.returnValue((events, end_key))
def get_current_key(self):
return self.store.get_room_events_max_id()
@defer.inlineCallbacks
def get_pagination_rows(self, user, pagination_config, key):
from_token = pagination_config.from_token
to_token = pagination_config.to_token
limit = pagination_config.limit
direction = pagination_config.direction
to_key = to_token.room_key if to_token else None
events, next_key = yield self.store.paginate_room_events(
room_id=key,
from_key=from_token.room_key,
to_key=to_key,
direction=direction,
limit=limit,
with_feedback=True
)
next_token = from_token.copy_and_replace("room_key", next_key)
defer.returnValue((events, next_token))

View File

@ -145,3 +145,17 @@ class TypingNotificationHandler(BaseHandler):
typing):
# TODO(paul) steal this from presence.py
pass
class TypingNotificationEventSource(object):
def __init__(self, hs):
self.hs = hs
def get_new_events_for_user(self, user, from_key, limit):
return ([], from_key)
def get_current_key(self):
return 0
def get_pagination_rows(self, user, pagination_config, key):
return ([], pagination_config.from_token)

View File

@ -325,7 +325,7 @@ class ContentRepoResource(resource.Resource):
# FIXME (erikj): These should use constants.
file_name = os.path.basename(fname)
url = "http://%s/matrix/content/%s" % (
url = "http://%s/_matrix/content/%s" % (
self.hs.domain_with_port, file_name
)

View File

@ -95,7 +95,7 @@ class Notifier(object):
"""
room_id = event.room_id
source = self.event_sources.sources["room"]
room_source = self.event_sources.sources["room"]
listeners = self.rooms_to_listeners.get(room_id, set()).copy()
@ -107,13 +107,17 @@ class Notifier(object):
# TODO (erikj): Can we make this more efficient by hitting the
# db once?
for listener in listeners:
events, end_token = yield source.get_new_events_for_user(
events, end_key = yield room_source.get_new_events_for_user(
listener.user,
listener.from_token,
listener.from_token.room_key,
listener.limit,
)
if events:
end_token = listener.from_token.copy_and_replace(
"room_key", end_key
)
listener.notify(
self, events, listener.from_token, end_token
)
@ -126,7 +130,7 @@ class Notifier(object):
Will wake up all listeners for the given users and rooms.
"""
source = self.event_sources.sources["presence"]
presence_source = self.event_sources.sources["presence"]
listeners = set()
@ -137,13 +141,17 @@ class Notifier(object):
listeners |= self.rooms_to_listeners.get(room, set()).copy()
for listener in listeners:
events, end_token = yield source.get_new_events_for_user(
events, end_key = yield presence_source.get_new_events_for_user(
listener.user,
listener.from_token,
listener.from_token.presence_key,
listener.limit,
)
if events:
end_token = listener.from_token.copy_and_replace(
"presence_key", end_key
)
listener.notify(
self, events, listener.from_token, end_token
)
@ -216,16 +224,18 @@ class Notifier(object):
limit = listener.limit
# TODO (erikj): DeferredList?
for source in self.event_sources.sources.values():
stuff, new_token = yield source.get_new_events_for_user(
for name, source in self.event_sources.sources.items():
keyname = "%s_key" % name
stuff, new_key = yield source.get_new_events_for_user(
listener.user,
from_token,
getattr(from_token, keyname),
limit,
)
events.extend(stuff)
from_token = new_token
from_token = from_token.copy_and_replace(keyname, new_key)
end_token = from_token

View File

@ -17,6 +17,10 @@ from twisted.internet import defer
from synapse.types import StreamToken
from synapse.handlers.presence import PresenceEventSource
from synapse.handlers.room import RoomEventSource
from synapse.handlers.typing import TypingNotificationEventSource
class NullSource(object):
"""This event source never yields any events and its token remains at
@ -24,146 +28,21 @@ class NullSource(object):
def __init__(self, hs):
pass
def get_new_events_for_user(self, user, from_token, limit):
return defer.succeed(([], from_token))
def get_new_events_for_user(self, user, from_key, limit):
return defer.succeed(([], from_key))
def get_current_token_part(self):
def get_current_key(self):
return defer.succeed(0)
def get_pagination_rows(self, user, pagination_config, key):
return defer.succeed(([], pagination_config.from_token))
class RoomEventSource(object):
def __init__(self, hs):
self.store = hs.get_datastore()
@defer.inlineCallbacks
def get_new_events_for_user(self, user, from_token, limit):
# We just ignore the key for now.
to_key = yield self.get_current_token_part()
events, end_key = yield self.store.get_room_events_stream(
user_id=user.to_string(),
from_key=from_token.events_key,
to_key=to_key,
room_id=None,
limit=limit,
)
end_token = from_token.copy_and_replace("events_key", end_key)
defer.returnValue((events, end_token))
def get_current_token_part(self):
return self.store.get_room_events_max_id()
@defer.inlineCallbacks
def get_pagination_rows(self, user, pagination_config, key):
from_token = pagination_config.from_token
to_token = pagination_config.to_token
limit = pagination_config.limit
direction = pagination_config.direction
to_key = to_token.events_key if to_token else None
events, next_key = yield self.store.paginate_room_events(
room_id=key,
from_key=from_token.events_key,
to_key=to_key,
direction=direction,
limit=limit,
with_feedback=True
)
next_token = from_token.copy_and_replace("events_key", next_key)
defer.returnValue((events, next_token))
class PresenceSource(object):
def __init__(self, hs):
self.hs = hs
self.clock = hs.get_clock()
def get_new_events_for_user(self, user, from_token, limit):
from_key = int(from_token.presence_key)
presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap
# TODO(paul): limit, and filter by visibility
updates = [(k, cachemap[k]) for k in cachemap
if from_key < cachemap[k].serial]
if updates:
clock = self.clock
latest_serial = max([x[1].serial for x in updates])
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
end_token = from_token.copy_and_replace(
"presence_key", latest_serial
)
return ((data, end_token))
else:
end_token = from_token.copy_and_replace(
"presence_key", presence._user_cachemap_latest_serial
)
return (([], end_token))
def get_current_token_part(self):
presence = self.hs.get_handlers().presence_handler
return presence._user_cachemap_latest_serial
def get_pagination_rows(self, user, pagination_config, key):
# TODO (erikj): Does this make sense? Ordering?
from_token = pagination_config.from_token
to_token = pagination_config.to_token
from_key = int(from_token.presence_key)
if to_token:
to_key = int(to_token.presence_key)
else:
to_key = -1
presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap
# TODO(paul): limit, and filter by visibility
updates = [(k, cachemap[k]) for k in cachemap
if to_key < cachemap[k].serial < from_key]
if updates:
clock = self.clock
earliest_serial = max([x[1].serial for x in updates])
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
if to_token:
next_token = to_token
else:
next_token = from_token
next_token = next_token.copy_and_replace(
"presence_key", earliest_serial
)
return ((data, next_token))
else:
if not to_token:
to_token = from_token.copy_and_replace(
"presence_key", 0
)
return (([], to_token))
class EventSources(object):
SOURCE_TYPES = {
"room": RoomEventSource,
"presence": PresenceSource,
"presence": PresenceEventSource,
"typing": TypingNotificationEventSource,
}
def __init__(self, hs):
@ -172,24 +51,29 @@ class EventSources(object):
for name, cls in EventSources.SOURCE_TYPES.items()
}
@staticmethod
def create_token(events_key, presence_key):
return StreamToken(events_key=events_key, presence_key=presence_key)
@defer.inlineCallbacks
def get_current_token(self):
events_key = yield self.sources["room"].get_current_token_part()
presence_key = yield self.sources["presence"].get_current_token_part()
token = EventSources.create_token(events_key, presence_key)
token = StreamToken(
room_key=(
yield self.sources["room"].get_current_key()
),
presence_key=(
yield self.sources["presence"].get_current_key()
),
typing_key=(
yield self.sources["typing"].get_current_key()
)
)
defer.returnValue(token)
class StreamSource(object):
def get_new_events_for_user(self, user, from_token, limit):
def get_new_events_for_user(self, user, from_key, limit):
"""from_key is the key within this event source."""
raise NotImplementedError("get_new_events_for_user")
def get_current_token_part(self):
raise NotImplementedError("get_current_token_part")
def get_current_key(self):
raise NotImplementedError("get_current_key")
def get_pagination_rows(self, user, pagination_config, key):
raise NotImplementedError("get_rows")

View File

@ -97,7 +97,7 @@ class RoomID(DomainSpecificString):
class StreamToken(
namedtuple(
"Token",
("events_key", "presence_key")
("room_key", "presence_key", "typing_key")
)
):
_SEPARATOR = "_"
@ -105,21 +105,14 @@ class StreamToken(
@classmethod
def from_string(cls, string):
try:
events_key, presence_key = string.split(cls._SEPARATOR)
keys = string.split(cls._SEPARATOR)
return cls(
events_key=events_key,
presence_key=presence_key,
)
return cls(*keys)
except:
raise SynapseError(400, "Invalid Token")
def to_string(self):
return "".join([
str(self.events_key),
self._SEPARATOR,
str(self.presence_key),
])
return self._SEPARATOR.join([str(k) for k in self])
def copy_and_replace(self, key, new_value):
d = self._asdict()

View File

@ -87,7 +87,7 @@ class FederationTestCase(unittest.TestCase):
# Empty context initially
(code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/state/my-context/", None)
"/_matrix/federation/v1/state/my-context/", None)
self.assertEquals(200, code)
self.assertFalse(response["pdus"])
@ -112,7 +112,7 @@ class FederationTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/state/my-context/", None)
"/_matrix/federation/v1/state/my-context/", None)
self.assertEquals(200, code)
self.assertEquals(1, len(response["pdus"]))
@ -123,7 +123,7 @@ class FederationTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/pdu/red/abc123def456/", None)
"/_matrix/federation/v1/pdu/red/abc123def456/", None)
self.assertEquals(404, code)
# Now insert such a PDU
@ -142,7 +142,7 @@ class FederationTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/pdu/red/abc123def456/", None)
"/_matrix/federation/v1/pdu/red/abc123def456/", None)
self.assertEquals(200, code)
self.assertEquals(1, len(response["pdus"]))
self.assertEquals("m.text", response["pdus"][0]["pdu_type"])
@ -168,7 +168,7 @@ class FederationTestCase(unittest.TestCase):
self.mock_http_client.put_json.assert_called_with(
"remote",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data={
"ts": 1000000,
"origin": "test",
@ -203,7 +203,7 @@ class FederationTestCase(unittest.TestCase):
# MockClock ensures we can guess these timestamps
self.mock_http_client.put_json.assert_called_with(
"remote",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data={
"origin": "test",
"ts": 1000000,
@ -226,7 +226,7 @@ class FederationTestCase(unittest.TestCase):
self.federation.register_edu_handler("m.test", recv_observer)
yield self.mock_resource.trigger("PUT",
"/matrix/federation/v1/send/1001000/",
"/_matrix/federation/v1/send/1001000/",
"""{
"origin": "remote",
"ts": 1001000,
@ -261,7 +261,7 @@ class FederationTestCase(unittest.TestCase):
self.mock_http_client.get_json.assert_called_with(
destination="remote",
path="/matrix/federation/v1/query/a-question",
path="/_matrix/federation/v1/query/a-question",
args={"one": "1", "two": "2"}
)
@ -273,7 +273,7 @@ class FederationTestCase(unittest.TestCase):
self.federation.register_query_handler("a-question", recv_handler)
code, response = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/query/a-question?three=3&four=4", None)
"/_matrix/federation/v1/query/a-question?three=3&four=4", None)
self.assertEquals(200, code)
self.assertEquals({"another": "response"}, response)

View File

@ -314,7 +314,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("elsewhere",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_invite",
content={
"observer_user": "@apple:test",
@ -340,7 +340,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("elsewhere",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_accept",
content={
"observer_user": "@cabbage:elsewhere",
@ -352,7 +352,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_invite",
content={
"observer_user": "@cabbage:elsewhere",
@ -371,7 +371,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("elsewhere",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_deny",
content={
"observer_user": "@cabbage:elsewhere",
@ -383,7 +383,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_invite",
content={
"observer_user": "@cabbage:elsewhere",
@ -397,7 +397,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_accepted_remote(self):
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_accept",
content={
"observer_user": "@apple:test",
@ -415,7 +415,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_denied_remote(self):
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_deny",
content={
"observer_user": "@apple:test",
@ -514,13 +514,6 @@ class PresencePushTestCase(unittest.TestCase):
)
hs.handlers = JustPresenceHandlers(hs)
def update(*args,**kwargs):
# print "mock_update_client: Args=%s, kwargs=%s" %(args, kwargs,)
return defer.succeed(None)
self.mock_update_client = Mock()
self.mock_update_client.side_effect = update
self.datastore = hs.get_datastore()
def get_received_txn_response(*args):
@ -528,7 +521,7 @@ class PresencePushTestCase(unittest.TestCase):
self.datastore.get_received_txn_response = get_received_txn_response
self.handler = hs.get_handlers().presence_handler
self.handler.push_update_to_clients = self.mock_update_client
self.event_source = hs.get_event_sources().sources["presence"]
# Mock the RoomMemberHandler
hs.handlers.room_member_handler = Mock(spec=[
@ -622,16 +615,23 @@ class PresencePushTestCase(unittest.TestCase):
apple_set.add(self.u_banana)
apple_set.add(self.u_clementine)
self.assertEquals(self.event_source.get_current_key(), 0)
yield self.handler.set_state(self.u_apple, self.u_apple,
{"state": ONLINE})
self.mock_update_client.assert_has_calls([
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
room_ids=["a-room"],
observed_user=self.u_apple,
statuscache=ANY), # self-reflection
], any_order=True)
self.mock_update_client.reset_mock()
self.assertEquals(self.event_source.get_current_key(), 1)
self.assertEquals(
self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
[
{"type": "m.presence",
"content": {
"user_id": "@apple:test",
"state": ONLINE,
"mtime_age": 0,
}},
],
)
presence = yield self.handler.get_presence_list(
observer_user=self.u_apple, accepted=True)
@ -657,31 +657,24 @@ class PresencePushTestCase(unittest.TestCase):
"state": OFFLINE},
], presence)
self.mock_update_client.assert_has_calls([
call(users_to_push=set([self.u_banana]),
room_ids=[],
observed_user=self.u_banana,
statuscache=ANY), # self-reflection
]) # and no others...
self.assertEquals(self.event_source.get_current_key(), 2)
self.assertEquals(
self.event_source.get_new_events_for_user(
self.u_banana, 1, None
)[0],
[
{"type": "m.presence",
"content": {
"user_id": "@banana:test",
"state": ONLINE,
"mtime_age": 2000
}},
]
)
@defer.inlineCallbacks
def test_push_remote(self):
put_json = self.mock_http_client.put_json
# put_json.expect_call_and_return(
# call("remote",
# path=ANY, # Can't guarantee which txn ID will be which
# data=_expect_edu("remote", "m.presence",
# content={
# "push": [
# {"user_id": "@apple:test",
# "state": "online",
# "mtime_age": 0},
# ],
# }
# )
# ),
# defer.succeed((200, "OK"))
# )
put_json.expect_call_and_return(
call("farm",
path=ANY, # Can't guarantee which txn ID will be which
@ -724,8 +717,10 @@ class PresencePushTestCase(unittest.TestCase):
self.room_members = [self.u_banana, self.u_potato]
self.assertEquals(self.event_source.get_current_key(), 0)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence",
content={
"push": [
@ -737,12 +732,20 @@ class PresencePushTestCase(unittest.TestCase):
)
)
self.mock_update_client.assert_has_calls([
call(users_to_push=set([self.u_apple]),
room_ids=["a-room"],
observed_user=self.u_potato,
statuscache=ANY),
], any_order=True)
self.assertEquals(self.event_source.get_current_key(), 1)
self.assertEquals(
self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)[0],
[
{"type": "m.presence",
"content": {
"user_id": "@potato:remote",
"state": ONLINE,
"mtime_age": 1000,
}}
]
)
self.clock.advance_time(2)
@ -754,24 +757,35 @@ class PresencePushTestCase(unittest.TestCase):
def test_join_room_local(self):
self.room_members = [self.u_apple, self.u_banana]
yield self.distributor.fire("user_joined_room", self.u_elderberry,
self.assertEquals(self.event_source.get_current_key(), 0)
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
self.handler._user_cachemap[self.u_clementine].update(
{
"state": PresenceState.ONLINE,
"mtime": self.clock.time_msec(),
}, self.u_clementine
)
yield self.distributor.fire("user_joined_room", self.u_clementine,
"a-room"
)
self.mock_update_client.assert_has_calls([
call(room_ids=["a-room"],
observed_user=self.u_elderberry,
users_to_push=set(),
statuscache=ANY),
call(users_to_push=set([self.u_elderberry]),
observed_user=self.u_apple,
room_ids=[],
statuscache=ANY),
call(users_to_push=set([self.u_elderberry]),
observed_user=self.u_banana,
room_ids=[],
statuscache=ANY),
], any_order=True)
self.assertEquals(self.event_source.get_current_key(), 1)
self.assertEquals(
self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)[0],
[
{"type": "m.presence",
"content": {
"user_id": "@clementine:test",
"state": ONLINE,
"mtime_age": 0,
}}
]
)
@defer.inlineCallbacks
def test_join_room_remote(self):
@ -822,7 +836,7 @@ class PresencePushTestCase(unittest.TestCase):
put_json.expect_call_and_return(
call("remote",
path="/matrix/federation/v1/send/1000002/",
path="/_matrix/federation/v1/send/1000002/",
data=_expect_edu("remote", "m.presence",
content={
"push": [
@ -1116,7 +1130,7 @@ class PresencePollingTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("remote",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("remote", "m.presence",
content={
"push": [
@ -1131,7 +1145,7 @@ class PresencePollingTestCase(unittest.TestCase):
)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("remote", "m.presence",
content={
"poll": [ "@banana:test" ],
@ -1145,7 +1159,7 @@ class PresencePollingTestCase(unittest.TestCase):
self.assertTrue(self.u_banana in self.handler._remote_sendmap)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000001/",
"/_matrix/federation/v1/send/1000001/",
_make_edu_json("remote", "m.presence",
content={
"unpoll": [ "@banana:test" ],

View File

@ -166,7 +166,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("farm",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("farm", "m.typing",
content={
"room_id": self.room_id,
@ -192,7 +192,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
self.room_members = [self.u_apple, self.u_onion]
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("farm", "m.typing",
content={
"room_id": self.room_id,
@ -216,7 +216,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("farm",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("farm", "m.typing",
content={
"room_id": self.room_id,

View File

@ -36,7 +36,7 @@ from mock import Mock
logging.getLogger().addHandler(logging.NullHandler())
PATH_PREFIX = "/matrix/client/api/v1"
PATH_PREFIX = "/_matrix/client/api/v1"
class EventStreamPaginationApiTestCase(unittest.TestCase):

View File

@ -37,7 +37,7 @@ ONLINE = PresenceState.ONLINE
myid = "@apple:test"
PATH_PREFIX = "/matrix/client/api/v1"
PATH_PREFIX = "/_matrix/client/api/v1"
class JustPresenceHandlers(object):
@ -229,7 +229,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
# HIDEOUS HACKERY
# TODO(paul): This should be injected in via the HomeServer DI system
from synapse.streams.events import (
PresenceSource, NullSource, EventSources
PresenceEventSource, NullSource, EventSources
)
old_SOURCE_TYPES = EventSources.SOURCE_TYPES
@ -240,7 +240,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
EventSources.SOURCE_TYPES = {
k: NullSource for k in old_SOURCE_TYPES.keys()
}
EventSources.SOURCE_TYPES["presence"] = PresenceSource
EventSources.SOURCE_TYPES["presence"] = PresenceEventSource
hs = HomeServer("test",
db_pool=None,
@ -274,6 +274,15 @@ class PresenceEventStreamTestCase(unittest.TestCase):
lambda u: defer.succeed([]))
self.mock_datastore = hs.get_datastore()
def get_profile_displayname(user_id):
return defer.succeed("Frank")
self.mock_datastore.get_profile_displayname = get_profile_displayname
def get_profile_avatar_url(user_id):
return defer.succeed(None)
self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url
self.presence = hs.get_handlers().presence_handler
self.u_apple = hs.parse_userid("@apple:test")
@ -295,7 +304,9 @@ class PresenceEventStreamTestCase(unittest.TestCase):
# all be ours
# I'll already get my own presence state change
self.assertEquals({"start": "0_1", "end": "0_1", "chunk": []}, response)
self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []},
response
)
self.mock_datastore.set_presence_state.return_value = defer.succeed(
{"state": ONLINE})
@ -306,14 +317,15 @@ class PresenceEventStreamTestCase(unittest.TestCase):
state={"state": ONLINE})
(code, response) = yield self.mock_resource.trigger("GET",
"/events?from=0_1&timeout=0", None)
"/events?from=0_1_0&timeout=0", None)
self.assertEquals(200, code)
self.assertEquals({"start": "0_1", "end": "0_2", "chunk": [
self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [
{"type": "m.presence",
"content": {
"user_id": "@banana:test",
"state": ONLINE,
"displayname": "Frank",
"mtime_age": 0,
}},
]}, response)

View File

@ -26,7 +26,7 @@ from synapse.api.errors import SynapseError, AuthError
from synapse.server import HomeServer
myid = "@1234ABCD:test"
PATH_PREFIX = "/matrix/client/api/v1"
PATH_PREFIX = "/_matrix/client/api/v1"
class ProfileTestCase(unittest.TestCase):
""" Tests profile management. """

View File

@ -32,7 +32,7 @@ from .utils import RestTestCase
from mock import Mock
PATH_PREFIX = "/matrix/client/api/v1"
PATH_PREFIX = "/_matrix/client/api/v1"
class RoomPermissionsTestCase(RestTestCase):

View File

@ -37,6 +37,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
mPresence.start();
}
$scope.user_id = matrixService.config().user_id;
/**
* Open a given page.
* @param {String} url url of the page
@ -45,6 +47,16 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
$location.url(url);
};
// Open the given user profile page
$scope.goToUserPage = function(user_id) {
if (user_id === $scope.user_id) {
$location.url("/settings");
}
else {
$location.url("/user/" + user_id);
}
};
// Logs the user out
$scope.logout = function() {
@ -69,11 +81,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
$scope.logout();
});
$scope.requestNotifications = function() {
if (window.Notification) {
console.log("Notification.permission: " + window.Notification.permission);
window.Notification.requestPermission(function(){});
}
$scope.updateHeader = function() {
$scope.user_id = matrixService.config().user_id;
};
}]);

View File

@ -32,7 +32,12 @@ angular.module('matrixWebClient')
.directive('ngFocus', ['$timeout', function($timeout) {
return {
link: function(scope, element, attr) {
$timeout(function() { element[0].focus(); }, 0);
// XXX: slightly evil hack to disable autofocus on iOS, as in general
// it causes more problems than it fixes, by bouncing the page
// around
if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) {
$timeout(function() { element[0].focus(); }, 0);
}
}
};
}]);

323
webclient/app.css Normal file → Executable file
View File

@ -1,121 +1,194 @@
/*** Mobile voodoo ***/
@media all and (max-device-width: 640px) {
#messageTableWrapper {
margin-right: 0px ! important;
}
.leftBlock {
width: 8em ! important;
font-size: 8px ! important;
}
.rightBlock {
width: 0px ! important;
display: none ! important;
}
.avatar {
width: 36px ! important;
}
#header,
#messageTable,
#wrapper,
#roomName,
#controls {
max-width: 640px ! important;
}
#userIdCell,
#usersTableWrapper,
#extraControls {
display: none;
}
#buttonsCell {
width: 60px ! important;
padding-left: 20px ! important;
}
#roomLogo {
display: none;
}
#roomName {
text-align: left ! important;
top: -35px ! important;
}
.bubble {
font-size: 12px ! important;
min-height: 20px ! important;
}
#page {
top: 35px ! important;
bottom: 70px ! important;
}
#header,
#page {
margin: 5px ! important;
}
#header {
padding: 5px ! important;
}
/* stop zoom on select */
select:focus,
textarea,
input
{
font-size: 16px ! important;
}
/** Common layout **/
html {
height: 100%;
}
body {
height: 100%;
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 0px;
}
h1 {
font-family: Helvetica, Arial, sans-serif;
font-size: 20pt;
}
/*** Overall page layout ***/
a:link { color: #666; }
a:visited { color: #666; }
a:hover { color: #000; }
a:active { color: #000; }
#page {
position: absolute;
top: 80px;
bottom: 100px;
left: 0px;
right: 0px;
margin: 20px;
min-height: 100%;
margin-bottom: -32px; /* to make room for the footer */
}
#wrapper {
margin: auto;
max-width: 1280px;
padding-top: 40px;
padding-bottom: 40px;
padding-left: 20px;
padding-right: 20px;
}
#header
{
position: absolute;
top: 0px;
width: 100%;
background-color: #333;
height: 32px;
}
#headerContent {
color: #ccc;
max-width: 1280px;
margin: auto;
text-align: right;
height: 32px;
line-height: 32px;
}
#headerContent a:link,
#headerContent a:visited,
#headerContent a:hover,
#headerContent a:active {
color: #fff;
}
#footer
{
width: 100%;
border-top: #666 1px solid;
background-color: #aaa;
height: 32px;
}
#footerContent
{
font-size: 8pt;
color: #fff;
max-width: 1280px;
margin: auto;
text-align: center;
height: 32px;
line-height: 32px;
}
#genericHeading
{
margin-top: 13px;
}
#feedback {
color: #800;
}
.mouse-pointer {
cursor: pointer;
}
.invited {
opacity: 0.2;
}
/*** Login Pages ***/
.loginWrapper {
text-align: center;
}
#loginForm {
text-align: left;
padding: 1em;
margin-bottom: 40px;
display: inline-block;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
#loginForm input[type='radio'] {
margin-right: 1em;
}
#serverConfig {
text-align: center;
}
#serverConfig,
#serverConfig input,
#serverConfig button
{
font-size: 10pt ! important;
}
.smallPrint {
color: #888;
font-size: 9pt ! important;
font-style: italic ! important;
}
#serverConfig label {
display: inline-block;
text-align: right;
margin-right: 0.5em;
width: 7em;
}
#loginForm,
#loginForm input,
#loginForm button,
#loginForm select {
font-size: 18px;
}
/*** Room page ***/
#roomPage {
position: absolute;
top: 120px;
bottom: 120px;
left: 20px;
right: 20px;
}
#roomWrapper {
margin: auto;
max-width: 1280px;
height: 100%;
}
#roomName {
max-width: 1280px;
width: 100%;
text-align: right;
top: -40px;
position: absolute;
float: right;
font-size: 16px;
margin-top: 15px;
}
#roomHeader {
margin: auto;
padding-left: 20px;
padding-right: 20px;
padding-top: 53px;
max-width: 1280px;
}
#controlPanel {
position: absolute;
bottom: 0px;
width: 100%;
height: 100px;
background-color: #f8f8f8;
border-top: #aaa 1px solid;
}
@ -146,10 +219,6 @@ h1 {
background-color: #faa;
}
.mouse-pointer {
cursor: pointer;
}
/*** Participant list ***/
#usersTableWrapper {
@ -300,7 +369,7 @@ h1 {
display: inline-block;
margin-bottom: -1px;
max-width: 90%;
font-size: 16px;
font-size: 14px;
word-wrap: break-word;
padding-top: 7px;
padding-bottom: 5px;
@ -310,6 +379,11 @@ h1 {
-webkit-text-size-adjust:100%
}
.bubble img {
max-width: 100%;
max-height: auto;
}
.differentUser td {
padding-bottom: 5px ! important;
}
@ -341,8 +415,8 @@ h1 {
}
#room-fullscreen-image img {
max-width: 100%;
max-height: 100%;
max-width: 90%;
max-height: 90%;
bottom: 0;
left: 0;
margin: auto;
@ -350,9 +424,14 @@ h1 {
position: fixed;
right: 0;
top: 0;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
}
/*** Recents ***/
.recentsTable {
max-width: 480px;
width: 100%;
@ -402,11 +481,14 @@ h1 {
}
/*** Recents in the room page ***/
#roomRecentsTableWrapper {
float: left;
max-width: 320px;
margin-right: 20px;
padding-right: 10px;
margin-right: 10px;
height: 100%;
border-right: 1px solid #ddd;
overflow-y: auto;
}
@ -421,55 +503,14 @@ h1 {
}
.profile-avatar img {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
object-fit: cover;
}
/*** User profile page ***/
#user-ids {
padding-left: 1em;
}
#user-displayname {
font-size: 24px;
}
/******************************/
#header
{
padding: 20px;
max-width: 1280px;
margin: auto;
}
#logo,
#roomLogo {
max-width: 1280px;
margin: auto;
}
#header-buttons {
float: right;
}
.text_entry_section {
position: fixed;
bottom: 0;
z-index: 100;
left: 0;
right: 10em;
width: 100%;
background: #e0e0e0;
}
.member_invited {
color: blue;
}
.member_joined {
}
.member_left {
color: gray;
}

View File

@ -18,6 +18,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
'ngRoute',
'MatrixWebClientController',
'LoginController',
'RegisterController',
'RoomController',
'HomeController',
'RecentsController',
@ -38,6 +39,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
templateUrl: 'login/login.html',
controller: 'LoginController'
}).
when('/register', {
templateUrl: 'login/register.html',
controller: 'RegisterController'
}).
when('/room/:room_id_or_alias', {
templateUrl: 'room/room.html',
controller: 'RoomController'
@ -84,7 +89,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) {
// If user auth details are not in cache, go to the login page
if (!matrixService.isUserLoggedIn()) {
if (!matrixService.isUserLoggedIn() &&
$location.path() !== "/login" &&
$location.path() !== "/register")
{
$location.path("login");
}

View File

@ -30,7 +30,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
*/
this.uploadFile = function(file) {
var deferred = $q.defer();
console.log("Uploading " + file.name + "... to /matrix/content");
console.log("Uploading " + file.name + "... to /_matrix/content");
matrixService.uploadContent(file).then(
function(response) {
var content_url = response.data.content_token;

View File

@ -36,7 +36,7 @@ var forAllTracksOnStream = function(s, f) {
}
angular.module('MatrixCall', [])
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) {
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope) {
var MatrixCall = function(room_id) {
this.room_id = room_id;
this.call_id = "c" + new Date().getTime();
@ -208,6 +208,7 @@ angular.module('MatrixCall', [])
// 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';
$rootScope.$apply();
}
};

View File

@ -38,7 +38,7 @@ angular.module('matrixService', [])
// Current version of permanent storage
var configVersion = 0;
var prefixPath = "/matrix/client/api/v1";
var prefixPath = "/_matrix/client/api/v1";
var MAPPING_PREFIX = "alias_for_";
var doRequest = function(method, path, params, data, $httpParams) {
@ -95,14 +95,18 @@ angular.module('matrixService', [])
},
// Create a room
create: function(room_id, visibility) {
create: function(room_alias, visibility) {
// The REST path spec
var path = "/createRoom";
return doRequest("POST", path, undefined, {
visibility: visibility,
room_alias_name: room_id
});
var req = {
"visibility": visibility
};
if (room_alias) {
req.room_alias_name = room_alias;
}
return doRequest("POST", path, undefined, req);
},
// List all rooms joined or been invited to
@ -164,7 +168,7 @@ angular.module('matrixService', [])
// Retrieves the room ID corresponding to a room alias
resolveRoomAlias:function(room_alias) {
var path = "/matrix/client/api/v1/directory/room/$room_alias";
var path = "/_matrix/client/api/v1/directory/room/$room_alias";
room_alias = encodeURIComponent(room_alias);
path = path.replace("$room_alias", room_alias);
@ -304,7 +308,7 @@ angular.module('matrixService', [])
// hit the Identity Server for a 3PID request.
linkEmail: function(email, clientSecret, sendAttempt) {
var path = "/matrix/identity/api/v1/validate/email/requestToken"
var path = "/_matrix/identity/api/v1/validate/email/requestToken"
var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
var headers = {};
headers["Content-Type"] = "application/x-www-form-urlencoded";
@ -312,7 +316,7 @@ angular.module('matrixService', [])
},
authEmail: function(clientSecret, tokenId, code) {
var path = "/matrix/identity/api/v1/validate/email/submitToken";
var path = "/_matrix/identity/api/v1/validate/email/submitToken";
var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
var headers = {};
headers["Content-Type"] = "application/x-www-form-urlencoded";
@ -320,7 +324,7 @@ angular.module('matrixService', [])
},
bindEmail: function(userId, tokenId, clientSecret) {
var path = "/matrix/identity/api/v1/3pid/bind";
var path = "/_matrix/identity/api/v1/3pid/bind";
var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
var headers = {};
headers["Content-Type"] = "application/x-www-form-urlencoded";
@ -328,7 +332,7 @@ angular.module('matrixService', [])
},
uploadContent: function(file) {
var path = "/matrix/content";
var path = "/_matrix/content";
var headers = {
"Content-Type": undefined // undefined means angular will figure it out
};

View File

@ -37,6 +37,11 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
$scope.joinAlias = {
room_alias: ""
};
$scope.profile = {
displayName: "",
avatarUrl: ""
};
var refresh = function() {
@ -53,14 +58,14 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
);
};
$scope.createNewRoom = function(room_id, isPrivate) {
$scope.createNewRoom = function(room_alias, isPrivate) {
var visibility = "public";
if (isPrivate) {
visibility = "private";
}
matrixService.create(room_id, visibility).then(
matrixService.create(room_alias, visibility).then(
function(response) {
// This room has been created. Refresh the rooms list
console.log("Created room " + response.data.room_alias + " with id: "+
@ -108,6 +113,26 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
};
$scope.onInit = function() {
// Load profile data
// Display name
matrixService.getDisplayName($scope.config.user_id).then(
function(response) {
$scope.profile.displayName = response.data.displayname;
},
function(error) {
$scope.feedback = "Can't load display name";
}
);
// Avatar
matrixService.getProfilePictureUrl($scope.config.user_id).then(
function(response) {
$scope.profile.avatarUrl = response.data.avatar_url;
},
function(error) {
$scope.feedback = "Can't load avatar URL";
}
);
refresh();
};
}]);

View File

@ -1,29 +1,24 @@
<div ng-controller="HomeController" data-ng-init="onInit()">
<div id="page">
<div id="wrapper">
<div>
<form>
<table>
<tr>
<td>
<div class="profile-avatar">
<img ng-src="{{ config.avatarUrl || 'img/default-profile.jpg' }}"/>
</div>
</td>
<td>
<div id="user-ids">
<div id="user-displayname">{{ config.displayName }}</div>
<div>{{ config.user_id }}</div>
</div>
</td>
</tr>
</table>
</form>
<div id="genericHeading">
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
</div>
<h3>Recents</h3>
<h1>Welcome to homeserver {{ config.homeserver }}</h1>
<div>
<div class="profile-avatar">
<img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}"/>
</div>
<div id="user-ids">
<div id="user-displayname">{{ profile.displayName }}</div>
<div>{{ config.user_id }}</div>
</div>
</div>
<h3>Recent conversations</h3>
<div ng-include="'recents/recents.html'"></div>
<br/>
@ -38,9 +33,9 @@
<div>
<form>
<input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
<input size="40" ng-model="newRoom.room_alias" ng-enter="createNewRoom(newRoom.room_alias, newRoom.private)" placeholder="(e.g. foo_channel)"/>
<input type="checkbox" ng-model="newRoom.private">private
<button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>
<button ng-disabled="!newRoom.room_alias" ng-click="createNewRoom(newRoom.room_alias, newRoom.private)">Create room</button>
</form>
</div>
<div>
@ -54,5 +49,4 @@
{{ feedback }}
</div>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

BIN
webclient/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -4,6 +4,8 @@
<title>[matrix]</title>
<link rel="stylesheet" href="app.css">
<link rel="stylesheet" href="mobile.css">
<link rel="icon" href="favicon.ico">
<meta name="viewport" content="width=device-width">
@ -19,6 +21,7 @@
<script src="app-filter.js"></script>
<script src="home/home-controller.js"></script>
<script src="login/login-controller.js"></script>
<script src="login/register-controller.js"></script>
<script src="recents/recents-controller.js"></script>
<script src="recents/recents-filter.js"></script>
<script src="room/room-controller.js"></script>
@ -38,15 +41,23 @@
<body>
<header id="header">
<div id="header">
<!-- Do not show buttons on the login page -->
<div id="header-buttons" ng-hide="'/login' == location ">
<div id="headerContent" ng-hide="'/login' == location || '/register' == location">
<a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a>
&nbsp;
<button ng-click='goToPage("/")'>Home</button>
<button ng-click='goToPage("settings")'>Settings</button>
<button ng-click="logout()">Log out</button>
</div>
</header>
</div>
<div ng-view></div>
<div id="page" ng-view></div>
<div id="footer" ng-hide="location.indexOf('/room') == 0">
<div id="footerContent">
&copy 2014 Matrix.org
</div>
</div>
</body>
</html>

View File

@ -1,3 +1,19 @@
/*
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.
*/
angular.module('LoginController', ['matrixService'])
.controller('LoginController', ['$scope', '$location', 'matrixService', 'eventStreamService',
function($scope, $location, matrixService, eventStreamService) {
@ -7,7 +23,10 @@ angular.module('LoginController', ['matrixService'])
// Assume that this is hosted on the home server, in which case the URL
// contains the home server.
var hs_url = $location.protocol() + "://" + $location.host();
if ($location.port()) {
if ($location.port() &&
!($location.protocol() === "http" && $location.port() === 80) &&
!($location.protocol() === "https" && $location.port() === 443))
{
hs_url += ":" + $location.port();
}
@ -16,57 +35,18 @@ angular.module('LoginController', ['matrixService'])
desired_user_name: "",
user_id: "",
password: "",
identityServer: "",
identityServer: "http://matrix.org:8090",
pwd1: "",
pwd2: ""
pwd2: "",
};
$scope.register = function() {
// Set the urls
matrixService.setConfig({
homeserver: $scope.account.homeserver,
identityServer: $scope.account.identityServer
});
if ($scope.account.pwd1 !== $scope.account.pwd2) {
$scope.feedback = "Passwords don't match.";
return;
}
else if ($scope.account.pwd1.length < 6) {
$scope.feedback = "Password must be at least 6 characters.";
return;
}
matrixService.register($scope.account.desired_user_name, $scope.account.pwd1).then(
function(response) {
$scope.feedback = "Success";
// Update the current config
var config = matrixService.config();
angular.extend(config, {
access_token: response.data.access_token,
user_id: response.data.user_id
});
matrixService.setConfig(config);
// And permanently save it
matrixService.saveConfig();
eventStreamService.resume();
// Go to the user's rooms list page
$location.url("home");
},
function(error) {
if (error.data) {
if (error.data.errcode === "M_USER_IN_USE") {
$scope.feedback = "Username already taken.";
}
}
else if (error.status === 0) {
$scope.feedback = "Unable to talk to the server.";
}
});
$scope.login_types = [ "email", "mxid" ];
$scope.login_type_label = {
"email": "Email address",
"mxid": "Matrix ID (e.g. @bob:matrix.org or bob)",
};
$scope.login_type = 'mxid'; // TODO: remember the user's preferred login_type
$scope.login = function() {
matrixService.setConfig({
homeserver: $scope.account.homeserver,

View File

@ -1,55 +1,49 @@
<div ng-controller="LoginController" class="login">
<h1 id="logo">[matrix]</h1>
<div id="page">
<div id="wrapper">
{{ feedback }}
<div id="wrapper" class="loginWrapper">
<h3>Register for an account:</h3>
<form novalidate>
<input id="desired_user_name" size="70" type="text" auto-focus ng-model="account.desired_user_name" placeholder="User name (ex:bob)"/>
<br/>
<input id="pwd1" size="70" type="password" auto-focus ng-model="account.pwd1" placeholder="Type a password"/>
<br/>
<input id="pwd2" size="70" type="password" auto-focus ng-model="account.pwd2" placeholder="Re-type your password"/>
<br/>
<!-- New user registration -->
<div>
<br/>
<button ng-click="register()" ng-disabled="!account.desired_user_name || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Register</button>
</div>
</form>
<h3>Got an account?</h3>
<form novalidate>
<!-- Login with an registered user -->
<div>{{ login_error_msg }} </div>
<div>
<input id="user_id" size="70" type="text" auto-focus ng-model="account.user_id" placeholder="User ID (ex:@bob:localhost or bob)"/>
<br />
<input id="password" size="70" type="password" ng-model="account.password" placeholder="Password"/><br />
<br/>
<button ng-click="login()" ng-disabled="!account.user_id || !account.password || !account.homeserver">Login</button>
</div>
</form>
<h3>Servers</h3>
<form novalidate>
<div>
Home Server:
<input id="homeserver" size="57" type="text" ng-model="account.homeserver" placeholder="Home server URL (ex: http://localhost:8080)"/>
</div>
<br />
<div>
Identity Server:
<input id="identityServer" size="56" type="text" ng-model="account.identityServer" placeholder="Identity server URL (ex: http://localhost:8090)"/>
</div>
<br />
</form>
<br/>
<a href ng-click="goToPage('/')">
<img src="img/logo.png" width="240" height="102" alt="[matrix]" style="padding: 50px"/>
</a>
<br/>
<form id="loginForm" novalidate>
<!-- Login with an registered user -->
<div>
Log in using:<br/>
<div ng-repeat="type in login_types">
<input type="radio" ng-model="$parent.login_type" value="{{ type }}" id="radio_{{ type }}"/>
<label for="radio_{{ type }}">{{ login_type_label[type] }}</label>
</div>
<div style="text-align: center">
<br/>
<input id="user_id" size="32" type="text" ng-focus="true" ng-model="account.user_id" placeholder="{{ login_type_label[login_type] }}"/>
<br/>
<input id="password" size="32" type="password" ng-model="account.password" placeholder="Password"/>
<br/><br/>
<button ng-click="login()" ng-disabled="!account.user_id || !account.password || !account.homeserver">Login</button>
<br/><br/>
</div>
<div class="feedback">{{ feedback }} {{ login_error_msg }}</div>
<div id="serverConfig">
<label for="homeserver">Home Server:</label>
<input id="homeserver" size="32" type="text" ng-model="account.homeserver" placeholder="URL (e.g. http://matrix.org:8080)"/>
<div class="smallPrint">Your home server stores all your conversation and account data.</div>
<label for="identityServer">Identity Server:</label>
<input id="identityServer" size="32" type="text" ng-model="account.identityServer" placeholder="URL (e.g. http://matrix.org:8090)"/>
<div class="smallPrint">Matrix provides identity servers to track which emails etc. belong to which Matrix IDs.<br/>
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>
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,102 @@
/*
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.
*/
angular.module('RegisterController', ['matrixService'])
.controller('RegisterController', ['$scope', '$location', 'matrixService', 'eventStreamService',
function($scope, $location, matrixService, eventStreamService) {
'use strict';
// FIXME: factor out duplication with login-controller.js
// Assume that this is hosted on the home server, in which case the URL
// contains the home server.
var hs_url = $location.protocol() + "://" + $location.host();
if ($location.port() &&
!($location.protocol() === "http" && $location.port() === 80) &&
!($location.protocol() === "https" && $location.port() === 443))
{
hs_url += ":" + $location.port();
}
$scope.account = {
homeserver: hs_url,
desired_user_id: "",
desired_user_name: "",
password: "",
identityServer: "http://matrix.org:8090",
pwd1: "",
pwd2: "",
displayName : ""
};
$scope.register = function() {
// Set the urls
matrixService.setConfig({
homeserver: $scope.account.homeserver,
identityServer: $scope.account.identityServer
});
if ($scope.account.pwd1 !== $scope.account.pwd2) {
$scope.feedback = "Passwords don't match.";
return;
}
else if ($scope.account.pwd1.length < 6) {
$scope.feedback = "Password must be at least 6 characters.";
return;
}
matrixService.register($scope.account.desired_user_id, $scope.account.pwd1).then(
function(response) {
$scope.feedback = "Success";
// Update the current config
var config = matrixService.config();
angular.extend(config, {
access_token: response.data.access_token,
user_id: response.data.user_id
});
matrixService.setConfig(config);
// And permanently save it
matrixService.saveConfig();
// Update the global scoped used_id var (used in the app header)
$scope.updateHeader();
eventStreamService.resume();
if ($scope.account.displayName) {
// FIXME: handle errors setting displayName
matrixService.setDisplayName($scope.account.displayName);
}
// Go to the user's rooms list page
$location.url("home");
},
function(error) {
if (error.data) {
if (error.data.errcode === "M_USER_IN_USE") {
$scope.feedback = "Username already taken.";
}
}
else if (error.status === 0) {
$scope.feedback = "Unable to talk to the server.";
}
});
};
}]);

View File

@ -0,0 +1,48 @@
<div ng-controller="RegisterController" class="register">
<div id="wrapper" class="loginWrapper">
<a href ng-click="goToPage('/')">
<img src="img/logo.png" width="240" height="102" alt="[matrix]" style="padding: 50px"/>
</a>
<br/>
<form id="loginForm" novalidate>
<div>
Create account:<br/>
<div style="text-align: center">
<br/>
<input id="email" size="32" type="text" ng-focus="true" ng-model="account.email" placeholder="Email address (optional)"/>
<div class="smallPrint">Specifying an email address lets other users find you on Matrix more easily,<br/>
and gives you a way to reset your password</div>
<input id="desired_user_id" size="32" type="text" ng-model="account.desired_user_id" placeholder="Matrix ID (e.g. bob)"/>
<br/>
<input id="pwd1" size="32" type="password" ng-model="account.pwd1" placeholder="Type a password"/>
<br/>
<input id="pwd2" size="32" type="password" ng-model="account.pwd2" placeholder="Confirm your password"/>
<br/>
<input id="displayName" size="32" type="text" ng-model="account.displayName" placeholder="Display name (e.g. Bob Obson)"/>
<br/>
<br/>
<button ng-click="register()" ng-disabled="!account.desired_user_id || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Sign up</button>
<br/><br/>
</div>
<div class="feedback">{{ feedback }} {{ login_error_msg }}</div>
<div id="serverConfig">
<label for="homeserver">Home Server:</label>
<input id="homeserver" size="32" type="text" ng-model="account.homeserver" placeholder="URL (e.g. http://matrix.org:8080)"/>
<div class="smallPrint">Your home server stores all your conversation and account data.</div>
<label for="identityServer">Identity Server:</label>
<input id="identityServer" size="32" type="text" ng-model="account.identityServer" placeholder="URL (e.g. http://matrix.org:8090)"/>
<div class="smallPrint">Matrix provides identity servers to track which emails etc. belong to which Matrix IDs.<br/>
Only http://matrix.org:8090 currently exists.</div>
</div>
</div>
</form>
</div>
</div>
</div>

92
webclient/mobile.css Normal file
View File

@ -0,0 +1,92 @@
/*** Mobile voodoo ***/
@media all and (max-device-width: 640px) {
#messageTableWrapper {
margin-right: 0px ! important;
}
.leftBlock {
width: 8em ! important;
font-size: 8px ! important;
}
.rightBlock {
width: 0px ! important;
display: none ! important;
}
.avatar {
width: 36px ! important;
}
#header {
background-color: transparent;
}
#headerContent {
padding-right: 5px;
}
#headerContent button {
font-size: 8px;
}
#messageTable,
#wrapper,
#controls {
max-width: 640px ! important;
}
#headerUserId,
#roomHeader img,
#userIdCell,
#roomRecentsTableWrapper,
#usersTableWrapper,
.extraControls {
display: none;
}
#buttonsCell {
width: 60px ! important;
padding-left: 20px ! important;
}
#roomLogo {
display: none;
}
.bubble {
font-size: 12px ! important;
min-height: 20px ! important;
}
#roomHeader {
padding-top: 10px;
}
#roomName {
float: left;
font-size: 14px ! important;
margin-top: 0px ! important;
}
#roomPage {
top: 35px ! important;
left: 5px ! important;
right: 5px ! important;
bottom: 70px ! important;
}
#controlPanel {
height: 70px;
}
/* stop zoom on select */
select:focus,
textarea,
input
{
font-size: 16px ! important;
}
}

View File

@ -88,7 +88,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
call.onHangup = $scope.onCallHangup;
$scope.currentCall = call;
});
$scope.memberCount = function() {
return Object.keys($scope.members).length;
};
@ -175,6 +175,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
// set target_user_id to keep things clear
var target_user_id = chunk.state_key;
var now = new Date().getTime();
var isNewMember = !(target_user_id in $scope.members);
if (isNewMember) {
@ -185,44 +187,14 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
if ("mtime_age" in chunk.content) {
chunk.mtime_age = chunk.content.mtime_age;
}
// Once the HS reliably returns the displaynames & avatar_urls for both
// local and remote users, we should use this rather than the evalAsync block
// below
if ("displayname" in chunk.content) {
chunk.displayname = chunk.content.displayname;
}
if ("avatar_url" in chunk.content) {
chunk.avatar_url = chunk.content.avatar_url;
}
$scope.members[target_user_id] = chunk;
/*
// Stale code for explicitly hammering the homeserver for every displayname & avatar_url
// get their display name and profile picture and set it to their
// member entry in $scope.members. We HAVE to use $timeout with 0 delay
// to make this function run AFTER the current digest cycle, else the
// response may update a STALE VERSION of the member list (manifesting
// as no member names appearing, or appearing sporadically).
$scope.$evalAsync(function() {
matrixService.getDisplayName(chunk.target_user_id).then(
function(response) {
var member = $scope.members[chunk.target_user_id];
if (member !== undefined) {
member.displayname = response.data.displayname;
}
}
);
matrixService.getProfilePictureUrl(chunk.target_user_id).then(
function(response) {
var member = $scope.members[chunk.target_user_id];
if (member !== undefined) {
member.avatar_url = response.data.avatar_url;
}
}
);
});
*/
chunk.last_updated = now;
$scope.members[target_user_id] = chunk;
if (target_user_id in $rootScope.presence) {
updatePresence($rootScope.presence[target_user_id]);
@ -234,6 +206,12 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
member.content.membership = chunk.content.membership;
}
};
var updateMemberListPresenceAge = function() {
$scope.now = new Date().getTime();
// TODO: don't bother polling every 5s if we know none of our counters are younger than 1 minute
$timeout(updateMemberListPresenceAge, 5 * 1000);
};
var updatePresence = function(chunk) {
if (!(chunk.content.user_id in $scope.members)) {
@ -275,6 +253,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
if ($scope.textInput.indexOf("/me") === 0) {
promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4));
}
else if ($scope.textInput.indexOf("/nick ") === 0) {
// Change user display name
promise = matrixService.setDisplayName($scope.textInput.substr(6));
}
else {
promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput);
}
@ -395,8 +377,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
// Make recents highlight the current room
$scope.recentsSelectedRoomID = $scope.room_id;
paginate(MESSAGES_PER_PAGINATION);
updateMemberListPresenceAge();
};
$scope.inviteUser = function(user_id) {
@ -404,18 +388,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
matrixService.invite($scope.room_id, user_id).then(
function() {
console.log("Invited.");
$scope.feedback = "Request for invitation succeeds";
$scope.feedback = "Invite sent successfully";
},
function(reason) {
$scope.feedback = "Failure: " + reason;
});
};
// Open the user profile page
$scope.goToUserPage = function(user_id) {
$location.url("/user/" + user_id);
};
$scope.leaveRoom = function() {
matrixService.leave($scope.room_id).then(
@ -487,7 +466,5 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
}
$scope.onCallHangup = function() {
$scope.feedback = "Call ended";
$scope.currentCall = undefined;
}
}]);

View File

@ -1,13 +1,15 @@
<div ng-controller="RoomController" data-ng-init="onInit()" class="room">
<h1 id="roomLogo">[matrix]</h1>
<div ng-controller="RoomController" data-ng-init="onInit()" class="room" style="height: 100%;">
<div id="page">
<div id="wrapper">
<div id="roomName">
{{ room_alias || room_id }}
<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_alias || room_id }}
</div>
</div>
<div id="roomPage">
<div id="roomWrapper">
<div id="roomRecentsTableWrapper">
<div ng-include="'recents/recents.html'"></div>
</div>
@ -15,17 +17,17 @@
<div id="usersTableWrapper">
<table id="usersTable">
<tr ng-repeat="member in members | orderMembersList">
<td class="userAvatar mouse-pointer" ng-click="goToUserPage(member.id)">
<td class="userAvatar mouse-pointer" ng-click="$parent.goToUserPage(member.id)" ng-class="member.membership == 'invite' ? 'invited' : ''">
<img class="userAvatarImage"
ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}"
ng-src="{{member.avatar_url || 'img/default-profile.png'}}"
alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
title="{{ member.id }}"
width="80" height="80"/>
<img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
<div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div>
</td>
<td class="userPresence" ng-class="member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')">
{{ member.mtime_age | duration }}<br/>{{ member.mtime_age ? "ago" : "" }}
<td class="userPresence" ng-class="(member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
<span ng-show="member.mtime_age">{{ member.mtime_age + (now - member.last_updated) | duration }}<br/>ago</span>
</td>
</table>
</div>
@ -40,7 +42,7 @@
<div class="timestamp">{{ (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.jpg' }}" width="32" height="32"
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32"
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
</td>
<td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
@ -64,7 +66,7 @@
</div>
</td>
<td class="rightBlock">
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32"
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
</td>
</tr>
@ -86,12 +88,12 @@
</td>
<td id="buttonsCell">
<button ng-click="send()">Send</button>
<button m-file-input="imageFileToSend">Image</button>
<button m-file-input="imageFileToSend" class="extraControls">Image</button>
</td>
</tr>
</table>
<div id="extraControls">
<div class="extraControls">
<span>
Invite a user:
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
@ -105,6 +107,10 @@
<button ng-click="hangupCall()">Reject</button>
</div>
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing'">Hang up</button>
<span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
<span ng-show="currentCall.state == 'ended'">Call Ended</span>
<span style="display: none; ">{{ currentCall.state }}</span>
</div>

View File

@ -22,8 +22,38 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
$scope.config = matrixService.config();
$scope.profile = {
displayName: $scope.config.displayName,
avatarUrl: $scope.config.avatarUrl
displayName: "",
avatarUrl: ""
};
// The profile as stored on the server
$scope.profileOnServer = {
displayName: "",
avatarUrl: ""
};
$scope.onInit = function() {
// Load profile data
// Display name
matrixService.getDisplayName($scope.config.user_id).then(
function(response) {
$scope.profile.displayName = response.data.displayname;
$scope.profileOnServer.displayName = response.data.displayname;
},
function(error) {
$scope.feedback = "Can't load display name";
}
);
// Avatar
matrixService.getProfilePictureUrl($scope.config.user_id).then(
function(response) {
$scope.profile.avatarUrl = response.data.avatar_url;
$scope.profileOnServer.avatarUrl = response.data.avatar_url;
},
function(error) {
$scope.feedback = "Can't load avatar URL";
}
);
};
$scope.$watch("profile.avatarFile", function(newValue, oldValue) {
@ -41,10 +71,10 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
});
$scope.saveProfile = function() {
if ($scope.profile.displayName !== $scope.config.displayName) {
if ($scope.profile.displayName !== $scope.profileOnServer.displayName) {
setDisplayName($scope.profile.displayName);
}
if ($scope.profile.avatarUrl !== $scope.config.avatarUrl) {
if ($scope.profile.avatarUrl !== $scope.profileOnServer.avatarUrl) {
setAvatar($scope.profile.avatarUrl);
}
};
@ -53,11 +83,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
matrixService.setDisplayName(displayName).then(
function(response) {
$scope.feedback = "Updated display name.";
var config = matrixService.config();
config.displayName = displayName;
matrixService.setConfig(config);
matrixService.saveConfig();
},
function(error) {
$scope.feedback = "Can't update display name: " + error.data;
@ -71,11 +96,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
function(response) {
console.log("Updated avatar");
$scope.feedback = "Updated avatar.";
var config = matrixService.config();
config.avatarUrl = avatarURL;
matrixService.setConfig(config);
matrixService.saveConfig();
},
function(error) {
$scope.feedback = "Can't update avatar: " + error.data;
@ -143,4 +163,23 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
}
);
};
/*** Desktop notifications section ***/
$scope.settings = {
notifications: undefined
};
// If the browser supports it, check the desktop notification state
if ("Notification" in window) {
$scope.settings.notifications = window.Notification.permission;
}
$scope.requestNotifications = function() {
console.log("requestNotifications");
window.Notification.requestPermission(function (permission) {
console.log(" -> User decision: " + permission);
$scope.settings.notifications = permission;
});
};
}]);

View File

@ -1,35 +1,29 @@
<div ng-controller="SettingsController" class="user">
<div ng-controller="SettingsController" class="user" data-ng-init="onInit()">
<div id="page">
<div id="wrapper">
<h3>Me</h3>
<div>
<div id="genericHeading">
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
</div>
<h1>Settings</h1>
<div class="section">
<form>
<table>
<tr>
<td>
<div class="profile-avatar">
<img ng-src="{{ profile.avatarUrl || 'img/default-profile.jpg' }}" m-file-input="profile.avatarFile"/>
</div>
</td>
<td>
<div id="user-ids">
<input size="40" ng-model="profile.displayName" placeholder="Your name"/>
</div>
</td>
<td>
<button ng-disabled="(profile.displayName == config.displayName) && (profile.avatarUrl == config.avatarUrl)"
ng-click="saveProfile()">Save</button>
</td>
</tr>
</table>
<div class="profile-avatar">
<img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}" m-file-input="profile.avatarFile"/>
</div>
<div id="user-ids">
<input size="40" ng-model="profile.displayName" placeholder="Your display name"/>
<br/>
<button ng-disabled="(profile.displayName == profileOnServer.displayName) && (profile.avatarUrl == profileOnServer.avatarUrl)"
ng-click="saveProfile()">Save</button>
</div>
</form>
</div>
<br/>
<h3>Linked emails</h3>
<div>
<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)">
@ -52,22 +46,35 @@
</table>
</div>
<br/>
<h3>Configuration</h3>
<div>
<div>Home server: {{ config.homeserver }} </div>
<div>User ID: {{ config.user_id }} </div>
<div>Access token: {{ config.access_token }} </div>
<h3>Desktop notifications</h3>
<div class="section" ng-switch="settings.notifications">
<div ng-switch-when="granted">
Notifications are enabled.
</div>
<div ng-switch-when="denied">
You have denied permission for notifications.<br/>
To enable it, reset the notification setting for this web site into your browser settings.
</div>
<div ng-switch-when="default">
<button ng-click="requestNotifications()" style="font-size: 14pt">Enable desktop notifications</button>
</div>
<div ng-switch-default="">
Sorry, your browser does not support notifications.
</div>
</div>
<br/>
<div>
<div><button ng-click="requestNotifications()">Request notifications</button></div>
<h3>Configuration</h3>
<div class="section">
<div>Home server: {{ config.homeserver }} </div>
<div>Identity server: {{ config.identityServer }} </div>
<div>User ID: {{ config.user_id }} </div>
<div>Access token: {{ config.access_token }} </div>
</div>
<br/>
{{ feedback }}
</div>
</div>
</div>

View File

@ -25,14 +25,42 @@ angular.module('UserController', ['matrixService'])
avatar_url: undefined
};
$scope.user_id = matrixService.config().user_id;
matrixService.getDisplayName($scope.user.id).then(
function(response) {
$scope.user.displayname = response.data.displayname;
}
);
matrixService.getProfilePictureUrl($scope.user.id).then(
function(response) {
$scope.user.avatar_url = response.data.avatar_url;
}
);
$scope.messageUser = function() {
// FIXME: create a new room every time, for now
matrixService.create(null, 'private').then(
function(response) {
// This room has been created. Refresh the rooms list
var room_id = response.data.room_id;
console.log("Created room with id: "+ room_id);
matrixService.invite(room_id, $scope.user.id).then(
function() {
$scope.feedback = "Invite sent successfully";
$scope.$parent.goToPage("/room/" + room_id);
},
function(reason) {
$scope.feedback = "Failure: " + JSON.stringify(reason);
});
},
function(error) {
$scope.feedback = "Failure: " + JSON.stringify(error.data);
});
};
}]);

View File

@ -1,31 +1,25 @@
<div ng-controller="UserController" class="user">
<h1 id="logo">[matrix]</h1>
<div id="page">
<div id="wrapper">
<div>
<form>
<table>
<tr>
<td>
<div class="profile-avatar">
<img ng-src="{{ user.avatar_url || 'img/default-profile.jpg' }}"/>
</div>
</td>
<td>
<div id="user-ids">
<div id="user-displayname">{{ user.displayname }}</div>
<div>{{ user.id }}</div>
</div>
</td>
</tr>
</table>
</form>
<div id="genericHeading">
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
</div>
<h1>{{ user.displayname || user.id }}</h1>
<div>
<div class="profile-avatar">
<img ng-src="{{ user.avatar_url || 'img/default-profile.png' }}"/>
</div>
<div id="user-ids">
<div>{{ user.id }}</div>
</div>
</div>
<button ng-hide="user.id == user_id" ng-click="messageUser()" style="font-size: 14pt; margin-top: 40px; margin-bottom: 40px">Start chat</button>
<br/>
{{ feedback }}
</div>
</div>
</div>