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", "complete_usernames": "on",
"send_delivery_receipts": "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.event_stream_token = "END"
self.prompt = ">>> " self.prompt = ">>> "
@ -252,7 +252,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_emailrequest(self, args): 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, json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']}) headers={'Content-Type': ['application/x-www-form-urlencoded']})
@ -274,7 +274,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_emailvalidate(self, args): 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, json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']}) headers={'Content-Type': ['application/x-www-form-urlencoded']})
@ -294,7 +294,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_3pidbind(self, args): 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, json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']}) headers={'Content-Type': ['application/x-www-form-urlencoded']})
@ -360,14 +360,14 @@ class SynapseCmd(cmd.Cmd):
def _do_invite(self, roomid, userstring): def _do_invite(self, roomid, userstring):
if (not userstring.startswith('@') and if (not userstring.startswith('@') and
self._is_on("complete_usernames")): 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}) json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring})
mxid = None mxid = None
if 'mxid' in json_res and 'signatures' in json_res: 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 pubKey = None
pubKeyObj = yield self.http_client.do_request("GET", url) 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 The aim of registration is to get a user ID and access token which you will need
when accessing other APIs:: 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", "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:: 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" "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", "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 If you want to send a message to someone, you have to be in a room with them. To
create a room:: 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", "room_alias": "#tutorial:localhost",
@ -105,7 +105,7 @@ Sending messages
---------------- ----------------
You can now send messages to this room:: 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. NB: There are no limitations to the types of messages which can be exchanged.
The only requirement is that ``"msgtype"`` is specified. 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:: 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 This informs ``@myfriend:localhost`` of the room ID
``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room. ``!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 If you receive an invite, you can join the room by changing the membership to
join:: 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 NB: Only the person invited (``@myfriend:localhost``) can change the membership
state to ``"join"``. 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 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:: 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" "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 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:: 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 may just want the most recent message for each room. This can be achieved by
applying pagination stream parameters to this request:: 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 Once you know which rooms the client has previously interacted with, you need to
listen for incoming events. This can be done like so:: 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": [], "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 All paths MUST be namespaced by the version of the API being used. This should
be: be:
/matrix/client/api/v1 /_matrix/client/api/v1
All REST paths in this section MUST be prefixed with this. E.g. All REST paths in this section MUST be prefixed with this. E.g.
REST Path: /rooms/$room_id REST Path: /rooms/$room_id
Absolute Path: /matrix/client/api/v1/rooms/$room_id Absolute Path: /_matrix/client/api/v1/rooms/$room_id
Registration Registration
============ ============

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -155,7 +155,7 @@ Protocol URLs
All these URLs are namespaced within a prefix of 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": 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 @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. 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 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 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, 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 The following diagram shows an ``m.room.message`` event being sent in the room
``!qporfwt:matrix.org``:: ``!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 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 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 which can be used to publicise rooms. They are case-insensitive. Note that the mapping
room ID is not fixed, and may change over time to point to a different room ID. For this from a room alias to a room ID is not fixed, and may change over time to point to a
reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on different room ID. For this reason, Clients SHOULD resolve the room alias to a room ID
subsequent requests. 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." "key": "This is a put but it is missing a txnId."
} }
- TODO: All strings everywhere are UTF-8
Receiving live updates on a client Receiving live updates on a client
---------------------------------- ----------------------------------
- C-S longpoll event stream Clients can receive new events by long-polling the home server. This will hold open the
- Concept of start/end tokens. HTTP connection for a short period of time waiting for new events, returning early if an
- Mention /initialSync to get token. 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 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. - 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 This specification outlines several standard event types, all of which are
prefixed with ``m.`` prefixed with ``m.``
@ -244,7 +371,8 @@ State messages
- m.room.config - m.room.config
- m.room.invite_join - 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 Non-state messages
------------------ ------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -260,19 +260,18 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def user_joined_room(self, user, room_id): def user_joined_room(self, user, room_id):
if user.is_mine: 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( self.push_update_to_local_and_remote(
observed_user=user, observed_user=user,
room_ids=[room_id], room_ids=[room_id],
statuscache=self._get_or_offline_usercache(user), statuscache=statuscache,
)
else:
self.push_update_to_clients(
observed_user=user,
room_ids=[room_id],
statuscache=self._get_or_offline_usercache(user),
) )
# We also want to tell them about current presence of people. # 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): class UserPresenceCache(object):
"""Store an observed user's state and status message. """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) chunk = yield self.store.get_rooms(is_public=True)
# FIXME (erikj): START is no longer a valid value # FIXME (erikj): START is no longer a valid value
defer.returnValue({"start": "START", "end": "END", "chunk": chunk}) 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): typing):
# TODO(paul) steal this from presence.py # TODO(paul) steal this from presence.py
pass 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. # FIXME (erikj): These should use constants.
file_name = os.path.basename(fname) 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 self.hs.domain_with_port, file_name
) )

View File

@ -95,7 +95,7 @@ class Notifier(object):
""" """
room_id = event.room_id 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() 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 # TODO (erikj): Can we make this more efficient by hitting the
# db once? # db once?
for listener in listeners: 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.user,
listener.from_token, listener.from_token.room_key,
listener.limit, listener.limit,
) )
if events: if events:
end_token = listener.from_token.copy_and_replace(
"room_key", end_key
)
listener.notify( listener.notify(
self, events, listener.from_token, end_token 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. 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() listeners = set()
@ -137,13 +141,17 @@ class Notifier(object):
listeners |= self.rooms_to_listeners.get(room, set()).copy() listeners |= self.rooms_to_listeners.get(room, set()).copy()
for listener in listeners: 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.user,
listener.from_token, listener.from_token.presence_key,
listener.limit, listener.limit,
) )
if events: if events:
end_token = listener.from_token.copy_and_replace(
"presence_key", end_key
)
listener.notify( listener.notify(
self, events, listener.from_token, end_token self, events, listener.from_token, end_token
) )
@ -216,16 +224,18 @@ class Notifier(object):
limit = listener.limit limit = listener.limit
# TODO (erikj): DeferredList? # TODO (erikj): DeferredList?
for source in self.event_sources.sources.values(): for name, source in self.event_sources.sources.items():
stuff, new_token = yield source.get_new_events_for_user( keyname = "%s_key" % name
stuff, new_key = yield source.get_new_events_for_user(
listener.user, listener.user,
from_token, getattr(from_token, keyname),
limit, limit,
) )
events.extend(stuff) events.extend(stuff)
from_token = new_token from_token = from_token.copy_and_replace(keyname, new_key)
end_token = from_token end_token = from_token

View File

@ -17,6 +17,10 @@ from twisted.internet import defer
from synapse.types import StreamToken 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): class NullSource(object):
"""This event source never yields any events and its token remains at """This event source never yields any events and its token remains at
@ -24,146 +28,21 @@ class NullSource(object):
def __init__(self, hs): def __init__(self, hs):
pass pass
def get_new_events_for_user(self, user, from_token, limit): def get_new_events_for_user(self, user, from_key, limit):
return defer.succeed(([], from_token)) return defer.succeed(([], from_key))
def get_current_token_part(self): def get_current_key(self):
return defer.succeed(0) return defer.succeed(0)
def get_pagination_rows(self, user, pagination_config, key): def get_pagination_rows(self, user, pagination_config, key):
return defer.succeed(([], pagination_config.from_token)) 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): class EventSources(object):
SOURCE_TYPES = { SOURCE_TYPES = {
"room": RoomEventSource, "room": RoomEventSource,
"presence": PresenceSource, "presence": PresenceEventSource,
"typing": TypingNotificationEventSource,
} }
def __init__(self, hs): def __init__(self, hs):
@ -172,24 +51,29 @@ class EventSources(object):
for name, cls in EventSources.SOURCE_TYPES.items() 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 @defer.inlineCallbacks
def get_current_token(self): def get_current_token(self):
events_key = yield self.sources["room"].get_current_token_part() token = StreamToken(
presence_key = yield self.sources["presence"].get_current_token_part() room_key=(
token = EventSources.create_token(events_key, presence_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) defer.returnValue(token)
class StreamSource(object): 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") raise NotImplementedError("get_new_events_for_user")
def get_current_token_part(self): def get_current_key(self):
raise NotImplementedError("get_current_token_part") raise NotImplementedError("get_current_key")
def get_pagination_rows(self, user, pagination_config, key): def get_pagination_rows(self, user, pagination_config, key):
raise NotImplementedError("get_rows") raise NotImplementedError("get_rows")

View File

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

View File

@ -87,7 +87,7 @@ class FederationTestCase(unittest.TestCase):
# Empty context initially # Empty context initially
(code, response) = yield self.mock_resource.trigger("GET", (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(200, code)
self.assertFalse(response["pdus"]) self.assertFalse(response["pdus"])
@ -112,7 +112,7 @@ class FederationTestCase(unittest.TestCase):
) )
(code, response) = yield self.mock_resource.trigger("GET", (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(200, code)
self.assertEquals(1, len(response["pdus"])) self.assertEquals(1, len(response["pdus"]))
@ -123,7 +123,7 @@ class FederationTestCase(unittest.TestCase):
) )
(code, response) = yield self.mock_resource.trigger("GET", (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) self.assertEquals(404, code)
# Now insert such a PDU # Now insert such a PDU
@ -142,7 +142,7 @@ class FederationTestCase(unittest.TestCase):
) )
(code, response) = yield self.mock_resource.trigger("GET", (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(200, code)
self.assertEquals(1, len(response["pdus"])) self.assertEquals(1, len(response["pdus"]))
self.assertEquals("m.text", response["pdus"][0]["pdu_type"]) 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( self.mock_http_client.put_json.assert_called_with(
"remote", "remote",
path="/matrix/federation/v1/send/1000000/", path="/_matrix/federation/v1/send/1000000/",
data={ data={
"ts": 1000000, "ts": 1000000,
"origin": "test", "origin": "test",
@ -203,7 +203,7 @@ class FederationTestCase(unittest.TestCase):
# MockClock ensures we can guess these timestamps # MockClock ensures we can guess these timestamps
self.mock_http_client.put_json.assert_called_with( self.mock_http_client.put_json.assert_called_with(
"remote", "remote",
path="/matrix/federation/v1/send/1000000/", path="/_matrix/federation/v1/send/1000000/",
data={ data={
"origin": "test", "origin": "test",
"ts": 1000000, "ts": 1000000,
@ -226,7 +226,7 @@ class FederationTestCase(unittest.TestCase):
self.federation.register_edu_handler("m.test", recv_observer) self.federation.register_edu_handler("m.test", recv_observer)
yield self.mock_resource.trigger("PUT", yield self.mock_resource.trigger("PUT",
"/matrix/federation/v1/send/1001000/", "/_matrix/federation/v1/send/1001000/",
"""{ """{
"origin": "remote", "origin": "remote",
"ts": 1001000, "ts": 1001000,
@ -261,7 +261,7 @@ class FederationTestCase(unittest.TestCase):
self.mock_http_client.get_json.assert_called_with( self.mock_http_client.get_json.assert_called_with(
destination="remote", destination="remote",
path="/matrix/federation/v1/query/a-question", path="/_matrix/federation/v1/query/a-question",
args={"one": "1", "two": "2"} args={"one": "1", "two": "2"}
) )
@ -273,7 +273,7 @@ class FederationTestCase(unittest.TestCase):
self.federation.register_query_handler("a-question", recv_handler) self.federation.register_query_handler("a-question", recv_handler)
code, response = yield self.mock_resource.trigger("GET", 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(200, code)
self.assertEquals({"another": "response"}, response) 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 = self.mock_http_client.put_json
put_json.expect_call_and_return( put_json.expect_call_and_return(
call("elsewhere", call("elsewhere",
path="/matrix/federation/v1/send/1000000/", path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_invite", data=_expect_edu("elsewhere", "m.presence_invite",
content={ content={
"observer_user": "@apple:test", "observer_user": "@apple:test",
@ -340,7 +340,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json put_json = self.mock_http_client.put_json
put_json.expect_call_and_return( put_json.expect_call_and_return(
call("elsewhere", call("elsewhere",
path="/matrix/federation/v1/send/1000000/", path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_accept", data=_expect_edu("elsewhere", "m.presence_accept",
content={ content={
"observer_user": "@cabbage:elsewhere", "observer_user": "@cabbage:elsewhere",
@ -352,7 +352,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
) )
yield self.mock_federation_resource.trigger("PUT", yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/", "/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_invite", _make_edu_json("elsewhere", "m.presence_invite",
content={ content={
"observer_user": "@cabbage:elsewhere", "observer_user": "@cabbage:elsewhere",
@ -371,7 +371,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json put_json = self.mock_http_client.put_json
put_json.expect_call_and_return( put_json.expect_call_and_return(
call("elsewhere", call("elsewhere",
path="/matrix/federation/v1/send/1000000/", path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_deny", data=_expect_edu("elsewhere", "m.presence_deny",
content={ content={
"observer_user": "@cabbage:elsewhere", "observer_user": "@cabbage:elsewhere",
@ -383,7 +383,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
) )
yield self.mock_federation_resource.trigger("PUT", yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/", "/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_invite", _make_edu_json("elsewhere", "m.presence_invite",
content={ content={
"observer_user": "@cabbage:elsewhere", "observer_user": "@cabbage:elsewhere",
@ -397,7 +397,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks @defer.inlineCallbacks
def test_accepted_remote(self): def test_accepted_remote(self):
yield self.mock_federation_resource.trigger("PUT", yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/", "/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_accept", _make_edu_json("elsewhere", "m.presence_accept",
content={ content={
"observer_user": "@apple:test", "observer_user": "@apple:test",
@ -415,7 +415,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks @defer.inlineCallbacks
def test_denied_remote(self): def test_denied_remote(self):
yield self.mock_federation_resource.trigger("PUT", yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/", "/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_deny", _make_edu_json("elsewhere", "m.presence_deny",
content={ content={
"observer_user": "@apple:test", "observer_user": "@apple:test",
@ -514,13 +514,6 @@ class PresencePushTestCase(unittest.TestCase):
) )
hs.handlers = JustPresenceHandlers(hs) 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() self.datastore = hs.get_datastore()
def get_received_txn_response(*args): 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.datastore.get_received_txn_response = get_received_txn_response
self.handler = hs.get_handlers().presence_handler 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 # Mock the RoomMemberHandler
hs.handlers.room_member_handler = Mock(spec=[ 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_banana)
apple_set.add(self.u_clementine) 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, yield self.handler.set_state(self.u_apple, self.u_apple,
{"state": ONLINE}) {"state": ONLINE})
self.mock_update_client.assert_has_calls([ self.assertEquals(self.event_source.get_current_key(), 1)
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]), self.assertEquals(
room_ids=["a-room"], self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
observed_user=self.u_apple, [
statuscache=ANY), # self-reflection {"type": "m.presence",
], any_order=True) "content": {
self.mock_update_client.reset_mock() "user_id": "@apple:test",
"state": ONLINE,
"mtime_age": 0,
}},
],
)
presence = yield self.handler.get_presence_list( presence = yield self.handler.get_presence_list(
observer_user=self.u_apple, accepted=True) observer_user=self.u_apple, accepted=True)
@ -657,31 +657,24 @@ class PresencePushTestCase(unittest.TestCase):
"state": OFFLINE}, "state": OFFLINE},
], presence) ], presence)
self.mock_update_client.assert_has_calls([ self.assertEquals(self.event_source.get_current_key(), 2)
call(users_to_push=set([self.u_banana]), self.assertEquals(
room_ids=[], self.event_source.get_new_events_for_user(
observed_user=self.u_banana, self.u_banana, 1, None
statuscache=ANY), # self-reflection )[0],
]) # and no others... [
{"type": "m.presence",
"content": {
"user_id": "@banana:test",
"state": ONLINE,
"mtime_age": 2000
}},
]
)
@defer.inlineCallbacks @defer.inlineCallbacks
def test_push_remote(self): def test_push_remote(self):
put_json = self.mock_http_client.put_json 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( put_json.expect_call_and_return(
call("farm", call("farm",
path=ANY, # Can't guarantee which txn ID will be which 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.room_members = [self.u_banana, self.u_potato]
self.assertEquals(self.event_source.get_current_key(), 0)
yield self.mock_federation_resource.trigger("PUT", yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/", "/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence", _make_edu_json("elsewhere", "m.presence",
content={ content={
"push": [ "push": [
@ -737,12 +732,20 @@ class PresencePushTestCase(unittest.TestCase):
) )
) )
self.mock_update_client.assert_has_calls([ self.assertEquals(self.event_source.get_current_key(), 1)
call(users_to_push=set([self.u_apple]), self.assertEquals(
room_ids=["a-room"], self.event_source.get_new_events_for_user(
observed_user=self.u_potato, self.u_apple, 0, None
statuscache=ANY), )[0],
], any_order=True) [
{"type": "m.presence",
"content": {
"user_id": "@potato:remote",
"state": ONLINE,
"mtime_age": 1000,
}}
]
)
self.clock.advance_time(2) self.clock.advance_time(2)
@ -754,24 +757,35 @@ class PresencePushTestCase(unittest.TestCase):
def test_join_room_local(self): def test_join_room_local(self):
self.room_members = [self.u_apple, self.u_banana] 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" "a-room"
) )
self.mock_update_client.assert_has_calls([ self.assertEquals(self.event_source.get_current_key(), 1)
call(room_ids=["a-room"], self.assertEquals(
observed_user=self.u_elderberry, self.event_source.get_new_events_for_user(
users_to_push=set(), self.u_apple, 0, None
statuscache=ANY), )[0],
call(users_to_push=set([self.u_elderberry]), [
observed_user=self.u_apple, {"type": "m.presence",
room_ids=[], "content": {
statuscache=ANY), "user_id": "@clementine:test",
call(users_to_push=set([self.u_elderberry]), "state": ONLINE,
observed_user=self.u_banana, "mtime_age": 0,
room_ids=[], }}
statuscache=ANY), ]
], any_order=True) )
@defer.inlineCallbacks @defer.inlineCallbacks
def test_join_room_remote(self): def test_join_room_remote(self):
@ -822,7 +836,7 @@ class PresencePushTestCase(unittest.TestCase):
put_json.expect_call_and_return( put_json.expect_call_and_return(
call("remote", call("remote",
path="/matrix/federation/v1/send/1000002/", path="/_matrix/federation/v1/send/1000002/",
data=_expect_edu("remote", "m.presence", data=_expect_edu("remote", "m.presence",
content={ content={
"push": [ "push": [
@ -1116,7 +1130,7 @@ class PresencePollingTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json put_json = self.mock_http_client.put_json
put_json.expect_call_and_return( put_json.expect_call_and_return(
call("remote", call("remote",
path="/matrix/federation/v1/send/1000000/", path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("remote", "m.presence", data=_expect_edu("remote", "m.presence",
content={ content={
"push": [ "push": [
@ -1131,7 +1145,7 @@ class PresencePollingTestCase(unittest.TestCase):
) )
yield self.mock_federation_resource.trigger("PUT", yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/", "/_matrix/federation/v1/send/1000000/",
_make_edu_json("remote", "m.presence", _make_edu_json("remote", "m.presence",
content={ content={
"poll": [ "@banana:test" ], "poll": [ "@banana:test" ],
@ -1145,7 +1159,7 @@ class PresencePollingTestCase(unittest.TestCase):
self.assertTrue(self.u_banana in self.handler._remote_sendmap) self.assertTrue(self.u_banana in self.handler._remote_sendmap)
yield self.mock_federation_resource.trigger("PUT", yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000001/", "/_matrix/federation/v1/send/1000001/",
_make_edu_json("remote", "m.presence", _make_edu_json("remote", "m.presence",
content={ content={
"unpoll": [ "@banana:test" ], "unpoll": [ "@banana:test" ],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,12 @@ angular.module('matrixWebClient')
.directive('ngFocus', ['$timeout', function($timeout) { .directive('ngFocus', ['$timeout', function($timeout) {
return { return {
link: function(scope, element, attr) { link: function(scope, element, attr) {
// 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); $timeout(function() { element[0].focus(); }, 0);
} }
}
}; };
}]); }]);

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

@ -1,121 +1,194 @@
/*** Mobile voodoo ***/ /** Common layout **/
@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;
}
html {
height: 100%;
} }
body { body {
height: 100%;
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif; font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt; font-size: 12pt;
margin: 0px; margin: 0px;
} }
h1 { 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 { #page {
position: absolute; min-height: 100%;
top: 80px; margin-bottom: -32px; /* to make room for the footer */
bottom: 100px;
left: 0px;
right: 0px;
margin: 20px;
} }
#wrapper { #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; margin: auto;
max-width: 1280px; max-width: 1280px;
height: 100%; height: 100%;
} }
#roomName { #roomName {
max-width: 1280px; float: right;
width: 100%;
text-align: right;
top: -40px;
position: absolute;
font-size: 16px; font-size: 16px;
margin-top: 15px;
}
#roomHeader {
margin: auto;
padding-left: 20px;
padding-right: 20px;
padding-top: 53px;
max-width: 1280px;
} }
#controlPanel { #controlPanel {
position: absolute; position: absolute;
bottom: 0px; bottom: 0px;
width: 100%; width: 100%;
height: 100px;
background-color: #f8f8f8; background-color: #f8f8f8;
border-top: #aaa 1px solid; border-top: #aaa 1px solid;
} }
@ -146,10 +219,6 @@ h1 {
background-color: #faa; background-color: #faa;
} }
.mouse-pointer {
cursor: pointer;
}
/*** Participant list ***/ /*** Participant list ***/
#usersTableWrapper { #usersTableWrapper {
@ -300,7 +369,7 @@ h1 {
display: inline-block; display: inline-block;
margin-bottom: -1px; margin-bottom: -1px;
max-width: 90%; max-width: 90%;
font-size: 16px; font-size: 14px;
word-wrap: break-word; word-wrap: break-word;
padding-top: 7px; padding-top: 7px;
padding-bottom: 5px; padding-bottom: 5px;
@ -310,6 +379,11 @@ h1 {
-webkit-text-size-adjust:100% -webkit-text-size-adjust:100%
} }
.bubble img {
max-width: 100%;
max-height: auto;
}
.differentUser td { .differentUser td {
padding-bottom: 5px ! important; padding-bottom: 5px ! important;
} }
@ -341,8 +415,8 @@ h1 {
} }
#room-fullscreen-image img { #room-fullscreen-image img {
max-width: 100%; max-width: 90%;
max-height: 100%; max-height: 90%;
bottom: 0; bottom: 0;
left: 0; left: 0;
margin: auto; margin: auto;
@ -350,9 +424,14 @@ h1 {
position: fixed; position: fixed;
right: 0; right: 0;
top: 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 ***/ /*** Recents ***/
.recentsTable { .recentsTable {
max-width: 480px; max-width: 480px;
width: 100%; width: 100%;
@ -402,11 +481,14 @@ h1 {
} }
/*** Recents in the room page ***/ /*** Recents in the room page ***/
#roomRecentsTableWrapper { #roomRecentsTableWrapper {
float: left; float: left;
max-width: 320px; max-width: 320px;
margin-right: 20px; padding-right: 10px;
margin-right: 10px;
height: 100%; height: 100%;
border-right: 1px solid #ddd;
overflow-y: auto; overflow-y: auto;
} }
@ -421,55 +503,14 @@ h1 {
} }
.profile-avatar img { .profile-avatar img {
max-width: 100%; width: 100%;
max-height: 100%; height: 100%;
object-fit: cover;
} }
/*** User profile page ***/ /*** User profile page ***/
#user-ids {
padding-left: 1em;
}
#user-displayname { #user-displayname {
font-size: 24px; 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', 'ngRoute',
'MatrixWebClientController', 'MatrixWebClientController',
'LoginController', 'LoginController',
'RegisterController',
'RoomController', 'RoomController',
'HomeController', 'HomeController',
'RecentsController', 'RecentsController',
@ -38,6 +39,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
templateUrl: 'login/login.html', templateUrl: 'login/login.html',
controller: 'LoginController' controller: 'LoginController'
}). }).
when('/register', {
templateUrl: 'login/register.html',
controller: 'RegisterController'
}).
when('/room/:room_id_or_alias', { when('/room/:room_id_or_alias', {
templateUrl: 'room/room.html', templateUrl: 'room/room.html',
controller: 'RoomController' controller: 'RoomController'
@ -84,7 +89,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) { matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) {
// If user auth details are not in cache, go to the login page // 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"); $location.path("login");
} }

View File

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

View File

@ -36,7 +36,7 @@ var forAllTracksOnStream = function(s, f) {
} }
angular.module('MatrixCall', []) 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) { var MatrixCall = function(room_id) {
this.room_id = room_id; this.room_id = room_id;
this.call_id = "c" + new Date().getTime(); 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 // 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') { if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
this.state = 'connected'; this.state = 'connected';
$rootScope.$apply();
} }
}; };

View File

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

View File

@ -38,6 +38,11 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
room_alias: "" room_alias: ""
}; };
$scope.profile = {
displayName: "",
avatarUrl: ""
};
var refresh = function() { var refresh = function() {
matrixService.publicRooms().then( matrixService.publicRooms().then(
@ -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"; var visibility = "public";
if (isPrivate) { if (isPrivate) {
visibility = "private"; visibility = "private";
} }
matrixService.create(room_id, visibility).then( matrixService.create(room_alias, visibility).then(
function(response) { function(response) {
// This room has been created. Refresh the rooms list // This room has been created. Refresh the rooms list
console.log("Created room " + response.data.room_alias + " with id: "+ console.log("Created room " + response.data.room_alias + " with id: "+
@ -108,6 +113,26 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
}; };
$scope.onInit = function() { $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(); refresh();
}; };
}]); }]);

View File

@ -1,29 +1,24 @@
<div ng-controller="HomeController" data-ng-init="onInit()"> <div ng-controller="HomeController" data-ng-init="onInit()">
<div id="page">
<div id="wrapper"> <div id="wrapper">
<div> <div id="genericHeading">
<form> <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
<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> </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> <div ng-include="'recents/recents.html'"></div>
<br/> <br/>
@ -38,9 +33,9 @@
<div> <div>
<form> <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 <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> </form>
</div> </div>
<div> <div>
@ -55,4 +50,3 @@
</div> </div>
</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> <title>[matrix]</title>
<link rel="stylesheet" href="app.css"> <link rel="stylesheet" href="app.css">
<link rel="stylesheet" href="mobile.css">
<link rel="icon" href="favicon.ico"> <link rel="icon" href="favicon.ico">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
@ -19,6 +21,7 @@
<script src="app-filter.js"></script> <script src="app-filter.js"></script>
<script src="home/home-controller.js"></script> <script src="home/home-controller.js"></script>
<script src="login/login-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-controller.js"></script>
<script src="recents/recents-filter.js"></script> <script src="recents/recents-filter.js"></script>
<script src="room/room-controller.js"></script> <script src="room/room-controller.js"></script>
@ -38,15 +41,23 @@
<body> <body>
<header id="header"> <div id="header">
<!-- Do not show buttons on the login page --> <!-- 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='goToPage("settings")'>Settings</button>
<button ng-click="logout()">Log out</button> <button ng-click="logout()">Log out</button>
</div> </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> </body>
</html> </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']) angular.module('LoginController', ['matrixService'])
.controller('LoginController', ['$scope', '$location', 'matrixService', 'eventStreamService', .controller('LoginController', ['$scope', '$location', 'matrixService', 'eventStreamService',
function($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 // Assume that this is hosted on the home server, in which case the URL
// contains the home server. // contains the home server.
var hs_url = $location.protocol() + "://" + $location.host(); 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(); hs_url += ":" + $location.port();
} }
@ -16,56 +35,17 @@ angular.module('LoginController', ['matrixService'])
desired_user_name: "", desired_user_name: "",
user_id: "", user_id: "",
password: "", password: "",
identityServer: "", identityServer: "http://matrix.org:8090",
pwd1: "", pwd1: "",
pwd2: "" pwd2: "",
}; };
$scope.register = function() { $scope.login_types = [ "email", "mxid" ];
$scope.login_type_label = {
// Set the urls "email": "Email address",
matrixService.setConfig({ "mxid": "Matrix ID (e.g. @bob:matrix.org or bob)",
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_type = 'mxid'; // TODO: remember the user's preferred login_type
$scope.login = function() { $scope.login = function() {
matrixService.setConfig({ matrixService.setConfig({

View File

@ -1,56 +1,50 @@
<div ng-controller="LoginController" class="login"> <div ng-controller="LoginController" class="login">
<h1 id="logo">[matrix]</h1> <div id="wrapper" class="loginWrapper">
<div id="page"> <a href ng-click="goToPage('/')">
<div id="wrapper"> <img src="img/logo.png" width="240" height="102" alt="[matrix]" style="padding: 50px"/>
</a>
{{ feedback }} <br/>
<h3>Register for an account:</h3> <form id="loginForm" novalidate>
<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 --> <!-- Login with an registered user -->
<div>{{ login_error_msg }} </div>
<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)"/> 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/> <br/>
<input id="password" size="70" type="password" ng-model="account.password" placeholder="Password"/><br /> <input id="user_id" size="32" type="text" ng-focus="true" ng-model="account.user_id" placeholder="{{ login_type_label[login_type] }}"/>
<br/> <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> <button ng-click="login()" ng-disabled="!account.user_id || !account.password || !account.homeserver">Login</button>
<br/><br/>
</div> </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> </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/>
</div> </div>
</div> </div>
</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

@ -176,6 +176,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
// set target_user_id to keep things clear // set target_user_id to keep things clear
var target_user_id = chunk.state_key; var target_user_id = chunk.state_key;
var now = new Date().getTime();
var isNewMember = !(target_user_id in $scope.members); var isNewMember = !(target_user_id in $scope.members);
if (isNewMember) { if (isNewMember) {
// FIXME: why are we copying these fields around inside chunk? // FIXME: why are we copying these fields around inside chunk?
@ -185,45 +187,15 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
if ("mtime_age" in chunk.content) { if ("mtime_age" in chunk.content) {
chunk.mtime_age = chunk.content.mtime_age; 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) { if ("displayname" in chunk.content) {
chunk.displayname = chunk.content.displayname; chunk.displayname = chunk.content.displayname;
} }
if ("avatar_url" in chunk.content) { if ("avatar_url" in chunk.content) {
chunk.avatar_url = chunk.content.avatar_url; chunk.avatar_url = chunk.content.avatar_url;
} }
chunk.last_updated = now;
$scope.members[target_user_id] = chunk; $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;
}
}
);
});
*/
if (target_user_id in $rootScope.presence) { if (target_user_id in $rootScope.presence) {
updatePresence($rootScope.presence[target_user_id]); updatePresence($rootScope.presence[target_user_id]);
} }
@ -235,6 +207,12 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
} }
}; };
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) { var updatePresence = function(chunk) {
if (!(chunk.content.user_id in $scope.members)) { if (!(chunk.content.user_id in $scope.members)) {
console.log("updatePresence: Unknown member for chunk " + JSON.stringify(chunk)); console.log("updatePresence: Unknown member for chunk " + JSON.stringify(chunk));
@ -275,6 +253,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
if ($scope.textInput.indexOf("/me") === 0) { if ($scope.textInput.indexOf("/me") === 0) {
promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4)); 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 { else {
promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput); promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput);
} }
@ -397,6 +379,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
$scope.recentsSelectedRoomID = $scope.room_id; $scope.recentsSelectedRoomID = $scope.room_id;
paginate(MESSAGES_PER_PAGINATION); paginate(MESSAGES_PER_PAGINATION);
updateMemberListPresenceAge();
}; };
$scope.inviteUser = function(user_id) { $scope.inviteUser = function(user_id) {
@ -404,18 +388,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
matrixService.invite($scope.room_id, user_id).then( matrixService.invite($scope.room_id, user_id).then(
function() { function() {
console.log("Invited."); console.log("Invited.");
$scope.feedback = "Request for invitation succeeds"; $scope.feedback = "Invite sent successfully";
}, },
function(reason) { function(reason) {
$scope.feedback = "Failure: " + reason; $scope.feedback = "Failure: " + reason;
}); });
}; };
// Open the user profile page
$scope.goToUserPage = function(user_id) {
$location.url("/user/" + user_id);
};
$scope.leaveRoom = function() { $scope.leaveRoom = function() {
matrixService.leave($scope.room_id).then( matrixService.leave($scope.room_id).then(
@ -487,7 +466,5 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
} }
$scope.onCallHangup = function() { $scope.onCallHangup = function() {
$scope.feedback = "Call ended";
$scope.currentCall = undefined;
} }
}]); }]);

View File

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

View File

@ -22,8 +22,38 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
$scope.config = matrixService.config(); $scope.config = matrixService.config();
$scope.profile = { $scope.profile = {
displayName: $scope.config.displayName, displayName: "",
avatarUrl: $scope.config.avatarUrl 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) { $scope.$watch("profile.avatarFile", function(newValue, oldValue) {
@ -41,10 +71,10 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
}); });
$scope.saveProfile = function() { $scope.saveProfile = function() {
if ($scope.profile.displayName !== $scope.config.displayName) { if ($scope.profile.displayName !== $scope.profileOnServer.displayName) {
setDisplayName($scope.profile.displayName); setDisplayName($scope.profile.displayName);
} }
if ($scope.profile.avatarUrl !== $scope.config.avatarUrl) { if ($scope.profile.avatarUrl !== $scope.profileOnServer.avatarUrl) {
setAvatar($scope.profile.avatarUrl); setAvatar($scope.profile.avatarUrl);
} }
}; };
@ -53,11 +83,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
matrixService.setDisplayName(displayName).then( matrixService.setDisplayName(displayName).then(
function(response) { function(response) {
$scope.feedback = "Updated display name."; $scope.feedback = "Updated display name.";
var config = matrixService.config();
config.displayName = displayName;
matrixService.setConfig(config);
matrixService.saveConfig();
}, },
function(error) { function(error) {
$scope.feedback = "Can't update display name: " + error.data; $scope.feedback = "Can't update display name: " + error.data;
@ -71,11 +96,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
function(response) { function(response) {
console.log("Updated avatar"); console.log("Updated avatar");
$scope.feedback = "Updated avatar."; $scope.feedback = "Updated avatar.";
var config = matrixService.config();
config.avatarUrl = avatarURL;
matrixService.setConfig(config);
matrixService.saveConfig();
}, },
function(error) { function(error) {
$scope.feedback = "Can't update avatar: " + error.data; $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"> <div id="wrapper">
<h3>Me</h3> <div id="genericHeading">
<div> <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> <form>
<table>
<tr>
<td>
<div class="profile-avatar"> <div class="profile-avatar">
<img ng-src="{{ profile.avatarUrl || 'img/default-profile.jpg' }}" m-file-input="profile.avatarFile"/> <img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}" m-file-input="profile.avatarFile"/>
</div> </div>
</td>
<td>
<div id="user-ids"> <div id="user-ids">
<input size="40" ng-model="profile.displayName" placeholder="Your name"/> <input size="40" ng-model="profile.displayName" placeholder="Your display name"/>
</div> <br/>
</td> <button ng-disabled="(profile.displayName == profileOnServer.displayName) && (profile.avatarUrl == profileOnServer.avatarUrl)"
<td>
<button ng-disabled="(profile.displayName == config.displayName) && (profile.avatarUrl == config.avatarUrl)"
ng-click="saveProfile()">Save</button> ng-click="saveProfile()">Save</button>
</td> </div>
</tr>
</table>
</form> </form>
</div> </div>
<br/> <br/>
<h3>Linked emails</h3> <h3>Linked emails</h3>
<div> <div class="section">
<form> <form>
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" /> <input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)"> <button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
@ -53,16 +47,30 @@
</div> </div>
<br/> <br/>
<h3>Configuration</h3> <h3>Desktop notifications</h3>
<div> <div class="section" ng-switch="settings.notifications">
<div>Home server: {{ config.homeserver }} </div> <div ng-switch-when="granted">
<div>User ID: {{ config.user_id }} </div> Notifications are enabled.
<div>Access token: {{ config.access_token }} </div> </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> </div>
<br/> <br/>
<div> <h3>Configuration</h3>
<div><button ng-click="requestNotifications()">Request notifications</button></div> <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> </div>
<br/> <br/>
@ -70,4 +78,3 @@
</div> </div>
</div> </div>
</div>

View File

@ -25,14 +25,42 @@ angular.module('UserController', ['matrixService'])
avatar_url: undefined avatar_url: undefined
}; };
$scope.user_id = matrixService.config().user_id;
matrixService.getDisplayName($scope.user.id).then( matrixService.getDisplayName($scope.user.id).then(
function(response) { function(response) {
$scope.user.displayname = response.data.displayname; $scope.user.displayname = response.data.displayname;
} }
); );
matrixService.getProfilePictureUrl($scope.user.id).then( matrixService.getProfilePictureUrl($scope.user.id).then(
function(response) { function(response) {
$scope.user.avatar_url = response.data.avatar_url; $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"> <div ng-controller="UserController" class="user">
<h1 id="logo">[matrix]</h1>
<div id="page">
<div id="wrapper"> <div id="wrapper">
<div> <div id="genericHeading">
<form> <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
<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> </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 }} {{ feedback }}
</div> </div>
</div> </div>
</div>