mirror of
				https://git.anonymousland.org/anonymousland/synapse.git
				synced 2025-10-31 13:51:59 -04:00 
			
		
		
		
	Merge branch 'develop' of github.com:matrix-org/synapse into room_config
This commit is contained in:
		
						commit
						10efca1a74
					
				
					 62 changed files with 1418 additions and 904 deletions
				
			
		
							
								
								
									
										7
									
								
								WISHLIST.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								WISHLIST.rst
									
										
									
									
									
										Normal 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 | ||||
|  | @ -60,7 +60,7 @@ class SynapseCmd(cmd.Cmd): | |||
|             "complete_usernames": "on", | ||||
|             "send_delivery_receipts": "on" | ||||
|         } | ||||
|         self.path_prefix = "/matrix/client/api/v1" | ||||
|         self.path_prefix = "/_matrix/client/api/v1" | ||||
|         self.event_stream_token = "END" | ||||
|         self.prompt = ">>> " | ||||
| 
 | ||||
|  | @ -252,7 +252,7 @@ class SynapseCmd(cmd.Cmd): | |||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def _do_emailrequest(self, args): | ||||
|         url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken" | ||||
|         url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/requestToken" | ||||
| 
 | ||||
|         json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, | ||||
|                                                      headers={'Content-Type': ['application/x-www-form-urlencoded']}) | ||||
|  | @ -274,7 +274,7 @@ class SynapseCmd(cmd.Cmd): | |||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def _do_emailvalidate(self, args): | ||||
|         url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken" | ||||
|         url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/submitToken" | ||||
| 
 | ||||
|         json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, | ||||
|                                                      headers={'Content-Type': ['application/x-www-form-urlencoded']}) | ||||
|  | @ -294,7 +294,7 @@ class SynapseCmd(cmd.Cmd): | |||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def _do_3pidbind(self, args): | ||||
|         url = self._identityServerUrl()+"/matrix/identity/api/v1/3pid/bind" | ||||
|         url = self._identityServerUrl()+"/_matrix/identity/api/v1/3pid/bind" | ||||
| 
 | ||||
|         json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, | ||||
|                                                      headers={'Content-Type': ['application/x-www-form-urlencoded']}) | ||||
|  | @ -360,14 +360,14 @@ class SynapseCmd(cmd.Cmd): | |||
|     def _do_invite(self, roomid, userstring): | ||||
|         if (not userstring.startswith('@') and | ||||
|                     self._is_on("complete_usernames")): | ||||
|             url = self._identityServerUrl()+"/matrix/identity/api/v1/lookup" | ||||
|             url = self._identityServerUrl()+"/_matrix/identity/api/v1/lookup" | ||||
| 
 | ||||
|             json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring}) | ||||
| 
 | ||||
|             mxid = None | ||||
| 
 | ||||
|             if 'mxid' in json_res and 'signatures' in json_res: | ||||
|                 url = self._identityServerUrl()+"/matrix/identity/api/v1/pubkey/ed25519" | ||||
|                 url = self._identityServerUrl()+"/_matrix/identity/api/v1/pubkey/ed25519" | ||||
| 
 | ||||
|                 pubKey = None | ||||
|                 pubKeyObj = yield self.http_client.do_request("GET", url) | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ Registration | |||
| The aim of registration is to get a user ID and access token which you will need | ||||
| when accessing other APIs:: | ||||
| 
 | ||||
|     curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/register" | ||||
|     curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/_matrix/client/api/v1/register" | ||||
| 
 | ||||
|     { | ||||
|         "access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",  | ||||
|  | @ -51,13 +51,13 @@ Login | |||
| ----- | ||||
| The aim when logging in is to get an access token for your existing user ID:: | ||||
| 
 | ||||
|     curl -XGET "http://localhost:8080/matrix/client/api/v1/login" | ||||
|     curl -XGET "http://localhost:8080/_matrix/client/api/v1/login" | ||||
| 
 | ||||
|     { | ||||
|         "type": "m.login.password" | ||||
|     } | ||||
| 
 | ||||
|     curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/login" | ||||
|     curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/_matrix/client/api/v1/login" | ||||
| 
 | ||||
|     { | ||||
|         "access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",  | ||||
|  | @ -87,7 +87,7 @@ Creating a room | |||
| If you want to send a message to someone, you have to be in a room with them. To | ||||
| create a room:: | ||||
| 
 | ||||
|     curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd" | ||||
|     curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/_matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd" | ||||
| 
 | ||||
|     { | ||||
|         "room_alias": "#tutorial:localhost",  | ||||
|  | @ -105,7 +105,7 @@ Sending messages | |||
| ---------------- | ||||
| You can now send messages to this room:: | ||||
| 
 | ||||
|     curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd" | ||||
|     curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd" | ||||
|      | ||||
| NB: There are no limitations to the types of messages which can be exchanged. | ||||
| The only requirement is that ``"msgtype"`` is specified. | ||||
|  | @ -127,7 +127,7 @@ Inviting a user to a room | |||
| ------------------------- | ||||
| You can directly invite a user to a room like so:: | ||||
| 
 | ||||
|     curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd" | ||||
|     curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd" | ||||
|      | ||||
| This informs ``@myfriend:localhost`` of the room ID  | ||||
| ``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room. | ||||
|  | @ -137,7 +137,7 @@ Joining a room via an invite | |||
| If you receive an invite, you can join the room by changing the membership to | ||||
| join:: | ||||
| 
 | ||||
|     curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK" | ||||
|     curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK" | ||||
|      | ||||
| NB: Only the person invited (``@myfriend:localhost``) can change the membership | ||||
| state to ``"join"``. | ||||
|  | @ -147,7 +147,7 @@ Joining a room via an alias | |||
| Alternatively, if you know the room alias for this room and the room config  | ||||
| allows it, you can directly join a room via the alias:: | ||||
| 
 | ||||
|     curl -XPUT -d '{}' "http://localhost:8080/matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK" | ||||
|     curl -XPUT -d '{}' "http://localhost:8080/_matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK" | ||||
|      | ||||
|     { | ||||
|         "room_id": "!CvcvRuDYDzTOzfKKgh:localhost" | ||||
|  | @ -173,7 +173,7 @@ Getting all state | |||
| If the client doesn't know any information on the rooms the user is  | ||||
| invited/joined on, they can get all the user's state for all rooms:: | ||||
| 
 | ||||
|     curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK" | ||||
|     curl -XGET "http://localhost:8080/_matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK" | ||||
|      | ||||
|     [ | ||||
|         { | ||||
|  | @ -236,7 +236,7 @@ all of the messages and feedback for these rooms. This can be a LOT of data. You | |||
| may just want the most recent message for each room. This can be achieved by  | ||||
| applying pagination stream parameters to this request:: | ||||
| 
 | ||||
|     curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1" | ||||
|     curl -XGET "http://localhost:8080/_matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1" | ||||
|      | ||||
|     [ | ||||
|         { | ||||
|  | @ -271,7 +271,7 @@ Getting live state | |||
| Once you know which rooms the client has previously interacted with, you need to | ||||
| listen for incoming events. This can be done like so:: | ||||
| 
 | ||||
|     curl -XGET "http://localhost:8080/matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END" | ||||
|     curl -XGET "http://localhost:8080/_matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END" | ||||
|      | ||||
|     { | ||||
|         "chunk": [],  | ||||
|  |  | |||
|  | @ -306,11 +306,11 @@ POST requests MUST be submitted as application/json. | |||
| All paths MUST be namespaced by the version of the API being used. This should | ||||
| be: | ||||
| 
 | ||||
| /matrix/client/api/v1 | ||||
| /_matrix/client/api/v1 | ||||
| 
 | ||||
| All REST paths in this section MUST be prefixed with this. E.g. | ||||
|   REST Path: /rooms/$room_id | ||||
|   Absolute Path: /matrix/client/api/v1/rooms/$room_id | ||||
|   Absolute Path: /_matrix/client/api/v1/rooms/$room_id | ||||
| 
 | ||||
| Registration | ||||
| ============ | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
|   "apiVersion": "1.0.0", | ||||
|   "swaggerVersion": "1.2", | ||||
|   "basePath": "http://localhost:8080/matrix/client/api/v1", | ||||
|   "basePath": "http://localhost:8080/_matrix/client/api/v1", | ||||
|   "resourcePath": "/directory", | ||||
|   "produces": [ | ||||
|     "application/json" | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
|   "apiVersion": "1.0.0", | ||||
|   "swaggerVersion": "1.2", | ||||
|   "basePath": "http://localhost:8080/matrix/client/api/v1", | ||||
|   "basePath": "http://localhost:8080/_matrix/client/api/v1", | ||||
|   "resourcePath": "/events", | ||||
|   "produces": [ | ||||
|     "application/json" | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ | |||
|       "path": "/login" | ||||
|     } | ||||
|   ],  | ||||
|   "basePath": "http://localhost:8080/matrix/client/api/v1",  | ||||
|   "basePath": "http://localhost:8080/_matrix/client/api/v1",  | ||||
|   "consumes": [ | ||||
|     "application/json" | ||||
|   ],  | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
|   "apiVersion": "1.0.0", | ||||
|   "swaggerVersion": "1.2", | ||||
|   "basePath": "http://localhost:8080/matrix/client/api/v1", | ||||
|   "basePath": "http://localhost:8080/_matrix/client/api/v1", | ||||
|   "resourcePath": "/presence", | ||||
|   "produces": [ | ||||
|     "application/json" | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
|   "apiVersion": "1.0.0", | ||||
|   "swaggerVersion": "1.2", | ||||
|   "basePath": "http://localhost:8080/matrix/client/api/v1", | ||||
|   "basePath": "http://localhost:8080/_matrix/client/api/v1", | ||||
|   "resourcePath": "/profile", | ||||
|   "produces": [ | ||||
|     "application/json" | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ | |||
|       "path": "/register" | ||||
|     } | ||||
|   ],  | ||||
|   "basePath": "http://localhost:8080/matrix/client/api/v1",  | ||||
|   "basePath": "http://localhost:8080/_matrix/client/api/v1",  | ||||
|   "consumes": [ | ||||
|     "application/json" | ||||
|   ],  | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
|   "apiVersion": "1.0.0", | ||||
|   "swaggerVersion": "1.2", | ||||
|   "basePath": "http://localhost:8080/matrix/client/api/v1",  | ||||
|   "basePath": "http://localhost:8080/_matrix/client/api/v1",  | ||||
|   "resourcePath": "/rooms", | ||||
|   "produces": [ | ||||
|     "application/json" | ||||
|  | @ -14,12 +14,12 @@ | |||
|   }, | ||||
|   "apis": [ | ||||
|     { | ||||
|       "path": "/rooms/{roomId}/send/{eventType}/{txnId}", | ||||
|       "path": "/rooms/{roomId}/send/{eventType}", | ||||
|       "operations": [ | ||||
|         { | ||||
|           "method": "PUT", | ||||
|           "method": "POST", | ||||
|           "summary": "Send a generic non-state event to this room.", | ||||
|           "notes": "This operation can also be done as a POST to /rooms/{roomId}/send/{eventType}", | ||||
|           "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", | ||||
|           "type": "EventId", | ||||
|           "nickname": "send_non_state_event", | ||||
|           "consumes": [ | ||||
|  | @ -46,13 +46,6 @@ | |||
|               "required": true, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             }, | ||||
|             { | ||||
|               "name": "txnId", | ||||
|               "description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.", | ||||
|               "required": true, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|  | @ -104,12 +97,12 @@ | |||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "path": "/rooms/{roomId}/send/m.room.message/{txnId}", | ||||
|       "path": "/rooms/{roomId}/send/m.room.message", | ||||
|       "operations": [ | ||||
|         { | ||||
|           "method": "PUT", | ||||
|           "method": "POST", | ||||
|           "summary": "Send a message in this room.", | ||||
|           "notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message", | ||||
|           "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", | ||||
|           "type": "EventId", | ||||
|           "nickname": "send_message", | ||||
|           "consumes": [ | ||||
|  | @ -129,13 +122,6 @@ | |||
|               "required": true, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             }, | ||||
|             { | ||||
|               "name": "txnId", | ||||
|               "description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.", | ||||
|               "required": true, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|  | @ -195,12 +181,12 @@ | |||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}", | ||||
|       "path": "/rooms/{roomId}/send/m.room.message.feedback", | ||||
|       "operations": [ | ||||
|         { | ||||
|           "method": "PUT", | ||||
|           "method": "POST", | ||||
|           "summary": "Send feedback to a message.", | ||||
|           "notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback", | ||||
|           "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", | ||||
|           "type": "EventId", | ||||
|           "nickname": "send_feedback", | ||||
|           "consumes": [ | ||||
|  | @ -220,13 +206,6 @@ | |||
|               "required": true, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             }, | ||||
|             { | ||||
|               "name": "txnId", | ||||
|               "description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.", | ||||
|               "required": true, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             } | ||||
|           ], | ||||
|           "responseMessages": [ | ||||
|  | @ -239,12 +218,12 @@ | |||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "path": "/rooms/{roomId}/invite/{txnId}", | ||||
|       "path": "/rooms/{roomId}/invite", | ||||
|       "operations": [ | ||||
|         { | ||||
|           "method": "PUT", | ||||
|           "method": "POST", | ||||
|           "summary": "Invite a user to this room.", | ||||
|           "notes": "This operation can also be done as a POST to /rooms/{roomId}/invite", | ||||
|           "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", | ||||
|           "type": "void", | ||||
|           "nickname": "invite", | ||||
|           "consumes": [ | ||||
|  | @ -258,13 +237,6 @@ | |||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             }, | ||||
|             { | ||||
|               "name": "txnId", | ||||
|               "description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ", | ||||
|               "required": false, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             }, | ||||
|             { | ||||
|               "name": "body", | ||||
|               "description": "The user to invite.", | ||||
|  | @ -277,12 +249,12 @@ | |||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "path": "/rooms/{roomId}/join/{txnId}", | ||||
|       "path": "/rooms/{roomId}/join", | ||||
|       "operations": [ | ||||
|         { | ||||
|           "method": "PUT", | ||||
|           "method": "POST", | ||||
|           "summary": "Join this room.", | ||||
|           "notes": "This operation can also be done as a POST to /rooms/{roomId}/join", | ||||
|           "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", | ||||
|           "type": "void", | ||||
|           "nickname": "join_room", | ||||
|           "consumes": [ | ||||
|  | @ -295,25 +267,18 @@ | |||
|               "required": true, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             }, | ||||
|             { | ||||
|               "name": "txnId", | ||||
|               "description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ", | ||||
|               "required": false, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             } | ||||
|           ] | ||||
|         }   | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "path": "/rooms/{roomId}/leave/{txnId}", | ||||
|       "path": "/rooms/{roomId}/leave", | ||||
|       "operations": [ | ||||
|         { | ||||
|           "method": "PUT", | ||||
|           "method": "POST", | ||||
|           "summary": "Leave this room.", | ||||
|           "notes": "This operation can also be done as a POST to /rooms/{roomId}/leave", | ||||
|           "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", | ||||
|           "type": "void", | ||||
|           "nickname": "leave", | ||||
|           "consumes": [ | ||||
|  | @ -326,13 +291,6 @@ | |||
|               "required": true, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             }, | ||||
|             { | ||||
|               "name": "txnId", | ||||
|               "description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ", | ||||
|               "required": false, | ||||
|               "type": "string", | ||||
|               "paramType": "path" | ||||
|             } | ||||
|           ] | ||||
|         }   | ||||
|  | @ -476,7 +434,7 @@ | |||
|           "parameters": [ | ||||
|             { | ||||
|               "name": "body", | ||||
|               "description": "The desired configuration for the room.", | ||||
|               "description": "The desired configuration for the room. This operation can also be done as a PUT by suffixing /{txnId}.", | ||||
|               "required": true, | ||||
|               "type": "RoomConfig", | ||||
|               "paramType": "body" | ||||
|  |  | |||
|  | @ -155,7 +155,7 @@ Protocol URLs | |||
| 
 | ||||
| All these URLs are namespaced within a prefix of  | ||||
| 
 | ||||
|   /matrix/federation/v1/... | ||||
|   /_matrix/federation/v1/... | ||||
| 
 | ||||
| For active pushing of messages representing live activity "as it happens": | ||||
| 
 | ||||
|  |  | |||
|  | @ -35,8 +35,8 @@ namespaced to the home server which allocated the account and looks like:: | |||
| 
 | ||||
|   @localpart:domain | ||||
| 
 | ||||
| The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user. | ||||
| 
 | ||||
| The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user. They are | ||||
| case-insensitive. | ||||
| 
 | ||||
| A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes. | ||||
| It is typically responsible for multiple clients. "Federation" is the term used to describe the | ||||
|  | @ -60,7 +60,8 @@ identified via a "Room ID", which look like:: | |||
| 
 | ||||
| There is exactly one room ID for each room. Whilst the room ID does contain a | ||||
| domain, it is simply for namespacing room IDs. The room does NOT reside on the | ||||
| domain specified. Room IDs are not meant to be human readable. | ||||
| domain specified. Room IDs are not meant to be human readable. They ARE | ||||
| case-sensitive. | ||||
| 
 | ||||
| The following diagram shows an ``m.room.message`` event being sent in the room  | ||||
| ``!qporfwt:matrix.org``:: | ||||
|  | @ -102,10 +103,10 @@ Each room can also have multiple "Room Aliases", which looks like:: | |||
| 
 | ||||
| A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained | ||||
| by visiting the domain specified. Room aliases are designed to be human readable strings | ||||
| which can be used to publicise rooms. Note that the mapping from a room alias to a  | ||||
| room ID is not fixed, and may change over time to point to a different room ID. For this | ||||
| reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on | ||||
| subsequent requests. | ||||
| which can be used to publicise rooms. They are case-insensitive. Note that the mapping  | ||||
| from a room alias to a room ID is not fixed, and may change over time to point to a  | ||||
| different room ID. For this reason, Clients SHOULD resolve the room alias to a room ID  | ||||
| once and then use that ID on subsequent requests. | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|  | @ -214,24 +215,150 @@ In contrast, these are invalid requests:: | |||
|       "key": "This is a put but it is missing a txnId." | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| - TODO: All strings everywhere are UTF-8 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| Receiving live updates on a client | ||||
| ---------------------------------- | ||||
| - C-S longpoll event stream | ||||
| - Concept of start/end tokens. | ||||
| - Mention /initialSync to get token. | ||||
| Clients can receive new events by long-polling the home server. This will hold open the | ||||
| HTTP connection for a short period of time waiting for new events, returning early if an | ||||
| event occurs. This is called the "Event Stream". All events which the client is authorised  | ||||
| to view will appear in the event stream. When the stream is closed, an ``end`` token is  | ||||
| returned. This token can be used in the next request to continue where the client left off. | ||||
| 
 | ||||
| When the client first logs in, they will need to initially synchronise with their home | ||||
| server. This is achieved via the ``/initialSync`` API. This API also returns an ``end`` | ||||
| token which can be used with the event stream. | ||||
| 
 | ||||
| Rooms | ||||
| ===== | ||||
| - How are they created? PDU anchor point: "root of the tree". | ||||
| 
 | ||||
| Creation | ||||
| -------- | ||||
| To create a room, a client has to use the ``/createRoom`` API. There are various options | ||||
| which can be set when creating a room: | ||||
| 
 | ||||
| ``visibility`` | ||||
|   Type:  | ||||
|     String | ||||
|   Optional:  | ||||
|     Yes | ||||
|   Value: | ||||
|     Either ``public`` or ``private``. | ||||
|   Description: | ||||
|     A ``public`` visibility indicates that the room will be shown in the public room list. A | ||||
|     ``private`` visibility will hide the room from the public room list. Rooms default to | ||||
|     ``public`` visibility if this key is not included. | ||||
| 
 | ||||
| ``room_alias_name`` | ||||
|   Type:  | ||||
|     String | ||||
|   Optional:  | ||||
|     Yes | ||||
|   Value: | ||||
|     The room alias localpart. | ||||
|   Description: | ||||
|     If this is included, a room alias will be created and mapped to the newly created room. | ||||
|     The alias will belong on the same home server which created the room, e.g. | ||||
|     ``!qadnasoi:domain.com >>> #room_alias_name:domain.com`` | ||||
| 
 | ||||
| Example:: | ||||
| 
 | ||||
|   { | ||||
|     "visibility": "public",  | ||||
|     "room_alias_name": "the pub" | ||||
|   } | ||||
| 
 | ||||
| - TODO: This creates a room creation event which serves as the root of the PDU graph for this room. | ||||
| 
 | ||||
| Modifying aliases | ||||
| ----------------- | ||||
| - Adding / removing aliases. | ||||
| - Invite/join dance | ||||
| - State and non-state data (+extensibility) | ||||
| 
 | ||||
| TODO : Room permissions / config / power levels. | ||||
| Permissions | ||||
| ----------- | ||||
| - TODO : Room permissions / config / power levels. What they are. How do they work. Examples. | ||||
| 
 | ||||
| Messages | ||||
| ======== | ||||
| Joining rooms | ||||
| ------------- | ||||
| - What is joining? What permissions / access does it give you? How does this affect /initialSync? | ||||
| - API to hit (``/join/$alias or id``). Explain how alias joining works (auto-resolving).  See "Room events" for more info. | ||||
| - What does the home server have to do? | ||||
| - Rooms that DON'T need an invite to join. This follows through onto inviting users section. | ||||
| - Outline invite join dance? | ||||
| 
 | ||||
| 
 | ||||
| Inviting users | ||||
| -------------- | ||||
| - Can invite users to a room if the room config key TODO is set to TODO. Must have required power level. | ||||
| - Outline invite join dance. What is it? Why is it required? How does it work? | ||||
| - What does the home server have to do? | ||||
| 
 | ||||
| The purpose of inviting users to a room is to notify them that the room exists  | ||||
| so they can choose to become a member of that room. Some rooms require that all  | ||||
| users who join a room are previously invited to it (an "invite-only" room).  | ||||
| Whether a given room is an "invite-only" room is determined by the room config  | ||||
| key ``TODO``. It can have one of the following values: | ||||
| 
 | ||||
|  - TODO Room config invite only value explanation | ||||
|  - TODO Room config free-to-join value explanation | ||||
| 
 | ||||
| Only users who have a membership state of ``join`` in a room can invite new  | ||||
| users to said room. The person being invited must not be in the ``join`` state  | ||||
| in the room. The fully-qualified user ID must be specified when inviting a user,  | ||||
| as the user may reside on a different home server. To invite a user, send the  | ||||
| following request to ``/rooms/<room id>/invite``, which will manage the  | ||||
| entire invitation process:: | ||||
| 
 | ||||
|   { | ||||
|     "user_id": "<user id to invite>" | ||||
|   } | ||||
| 
 | ||||
| Alternatively, the membership state for this user in this room can be modified  | ||||
| directly by sending the following request to  | ||||
| ``/rooms/<room id>/state/m.room.member/<url encoded user id>``:: | ||||
| 
 | ||||
|   { | ||||
|     "membership": "invite" | ||||
|   } | ||||
| 
 | ||||
| See the "Room events" section for more information on ``m.room.member``. | ||||
| 
 | ||||
| - TODO: In what circumstances will this NOT be equivalent to ``/invite``? | ||||
| 
 | ||||
| Leaving rooms | ||||
| ------------- | ||||
| - API to hit (``$roomid/leave``). See "Room events" for more info. | ||||
| - Must be joined to leave. How does this affect /initialSync? | ||||
| - Not ever being in a room is NOT equivalent to have left it (due to membership: leave). | ||||
| - Need to be re-invited if invite-only room. | ||||
| - If no more HSes in room, can delete room? | ||||
| - Is there a dance? | ||||
| 
 | ||||
| Events in a room | ||||
| ---------------- | ||||
| - Split into state and non-state data | ||||
| - Explain what they are, semantics, give examples of clobbering / not, use cases (msgs vs room names). | ||||
|   Not too much detail on the actual event contents. | ||||
| - API to hit. | ||||
| - Extensibility provided by the API for custom events. Examples. | ||||
| - How this hooks into ``initialSync``. | ||||
| - See the "Room Events" section for actual spec on each type. | ||||
| 
 | ||||
| Syncing a room | ||||
| -------------- | ||||
| - Single room initial sync. API to hit. Why it might be used (lazy loading) | ||||
| 
 | ||||
| Getting grouped state events | ||||
| ---------------------------- | ||||
| - ``/members`` and ``/messages`` and the events they return. | ||||
| - ``/state`` and it returns ALL THE THINGS. | ||||
| 
 | ||||
| Room Events | ||||
| =========== | ||||
| 
 | ||||
| This specification outlines several standard event types, all of which are | ||||
| prefixed with ``m.`` | ||||
|  | @ -244,7 +371,8 @@ State messages | |||
| - m.room.config | ||||
| - m.room.invite_join | ||||
| 
 | ||||
| What are they, when are they used, what do they contain, how should they be used | ||||
| What are they, when are they used, what do they contain, how should they be used. | ||||
| Link back to explanatory sections (e.g. invite/join/leave sections for m.room.member) | ||||
| 
 | ||||
| Non-state messages | ||||
| ------------------ | ||||
|  |  | |||
|  | @ -120,7 +120,7 @@ def make_graph(pdus, room, filename_prefix): | |||
| def get_pdus(host, room): | ||||
|     transaction = json.loads( | ||||
|         urllib2.urlopen( | ||||
|             "http://%s/matrix/federation/v1/context/%s/" % (host, room) | ||||
|             "http://%s/_matrix/federation/v1/context/%s/" % (host, room) | ||||
|         ).read() | ||||
|     ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ $('.login').live('click', function() { | |||
|     var user = $("#userLogin").val(); | ||||
|     var password = $("#passwordLogin").val(); | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/login", | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/login", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), | ||||
|  | @ -25,11 +25,12 @@ $('.login').live('click', function() { | |||
| }); | ||||
| 
 | ||||
| var getCurrentRoomList = function() { | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; | ||||
|     $.getJSON(url, function(data) { | ||||
|         for (var i=0; i<data.length; ++i) { | ||||
|             data[i].latest_message = data[i].messages.chunk[0].content.body; | ||||
|             addRoom(data[i]);    | ||||
|         var rooms = data.rooms; | ||||
|         for (var i=0; i<rooms.length; ++i) { | ||||
|             rooms[i].latest_message = rooms[i].messages.chunk[0].content.body; | ||||
|             addRoom(rooms[i]);    | ||||
|         } | ||||
|     }).fail(function(err) { | ||||
|         alert(JSON.stringify($.parseJSON(err.responseText))); | ||||
|  | @ -43,7 +44,7 @@ $('.createRoom').live('click', function() { | |||
|         data.room_alias_name = roomAlias;    | ||||
|     } | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token, | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify(data), | ||||
|  | @ -78,11 +79,9 @@ $('.sendMessage').live('click', function() { | |||
|         return; | ||||
|     } | ||||
|      | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; | ||||
|     url = url.replace("$token", accountInfo.access_token); | ||||
|     url = url.replace("$roomid", encodeURIComponent(roomId)); | ||||
|     url = url.replace("$user", encodeURIComponent(accountInfo.user_id)); | ||||
|     url = url.replace("$msgid", msgId); | ||||
|      | ||||
|     var data = { | ||||
|         msgtype: "m.text", | ||||
|  | @ -91,7 +90,7 @@ $('.sendMessage').live('click', function() { | |||
|      | ||||
|     $.ajax({ | ||||
|         url: url, | ||||
|         type: "PUT", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify(data), | ||||
|         dataType: "json", | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ var eventStreamInfo = { | |||
| var roomInfo = []; | ||||
| 
 | ||||
| var longpollEventStream = function() { | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from"; | ||||
|     url = url.replace("$token", accountInfo.access_token); | ||||
|     url = url.replace("$from", eventStreamInfo.from); | ||||
|      | ||||
|  | @ -48,7 +48,7 @@ $('.login').live('click', function() { | |||
|     var user = $("#userLogin").val(); | ||||
|     var password = $("#passwordLogin").val(); | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/login", | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/login", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), | ||||
|  | @ -65,14 +65,15 @@ $('.login').live('click', function() { | |||
| 
 | ||||
| var getCurrentRoomList = function() { | ||||
|     $("#roomId").val(""); | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; | ||||
|     $.getJSON(url, function(data) { | ||||
|         for (var i=0; i<data.length; ++i) { | ||||
|             if ("messages" in data[i]) { | ||||
|                 data[i].latest_message = data[i].messages.chunk[0].content.body;    | ||||
|         var rooms = data.rooms; | ||||
|         for (var i=0; i<rooms.length; ++i) { | ||||
|             if ("messages" in rooms[i]) { | ||||
|                 rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;    | ||||
|             } | ||||
|         } | ||||
|         roomInfo = data; | ||||
|         roomInfo = rooms; | ||||
|         setRooms(roomInfo);   | ||||
|     }).fail(function(err) { | ||||
|         alert(JSON.stringify($.parseJSON(err.responseText))); | ||||
|  | @ -92,17 +93,14 @@ $('.sendMessage').live('click', function() { | |||
| 
 | ||||
| var sendMessage = function(roomId) { | ||||
|     var body = "jsfiddle message @" + $.now(); | ||||
|     var msgId = $.now(); | ||||
|      | ||||
|     if (roomId.length === 0) { | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; | ||||
|     url = url.replace("$token", accountInfo.access_token); | ||||
|     url = url.replace("$roomid", encodeURIComponent(roomId)); | ||||
|     url = url.replace("$user", encodeURIComponent(accountInfo.user_id)); | ||||
|     url = url.replace("$msgid", msgId); | ||||
|      | ||||
|     var data = { | ||||
|         msgtype: "m.text", | ||||
|  | @ -111,7 +109,7 @@ var sendMessage = function(roomId) { | |||
|      | ||||
|     $.ajax({ | ||||
|         url: url, | ||||
|         type: "PUT", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify(data), | ||||
|         dataType: "json", | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ var viewingRoomId; | |||
| 
 | ||||
| // ************** Event Streaming **************
 | ||||
| var longpollEventStream = function() { | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from"; | ||||
|     url = url.replace("$token", accountInfo.access_token); | ||||
|     url = url.replace("$from", eventStreamInfo.from); | ||||
| 
 | ||||
|  | @ -38,8 +38,9 @@ var longpollEventStream = function() { | |||
|             else if (data.chunk[i].type === "m.room.member") { | ||||
|                 if (viewingRoomId === data.chunk[i].room_id) { | ||||
|                     console.log("Got new member: " + JSON.stringify(data.chunk[i])); | ||||
|                     addMessage(data.chunk[i]); | ||||
|                     for (j=0; j<memberInfo.length; ++j) { | ||||
|                         if (memberInfo[j].target_user_id === data.chunk[i].target_user_id) { | ||||
|                         if (memberInfo[j].state_key === data.chunk[i].state_key) { | ||||
|                             memberInfo[j] = data.chunk[i]; | ||||
|                             updatedMemberList = true; | ||||
|                             break; | ||||
|  | @ -50,7 +51,7 @@ var longpollEventStream = function() { | |||
|                         updatedMemberList = true; | ||||
|                     } | ||||
|                 } | ||||
|                 if (data.chunk[i].target_user_id === accountInfo.user_id) { | ||||
|                 if (data.chunk[i].state_key === accountInfo.user_id) { | ||||
|                     getCurrentRoomList(); // update our join/invite list
 | ||||
|                 } | ||||
|             } | ||||
|  | @ -88,7 +89,7 @@ $('.login').live('click', function() { | |||
|     var user = $("#userLogin").val(); | ||||
|     var password = $("#passwordLogin").val(); | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/login", | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/login", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), | ||||
|  | @ -106,7 +107,7 @@ $('.register').live('click', function() { | |||
|     var user = $("#userReg").val(); | ||||
|     var password = $("#passwordReg").val(); | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/register", | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/register", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify({ user_id: user, password: password }), | ||||
|  | @ -133,7 +134,7 @@ $('.createRoom').live('click', function() { | |||
|         data.room_alias_name = roomAlias;    | ||||
|     } | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token, | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify(data), | ||||
|  | @ -154,14 +155,15 @@ $('.createRoom').live('click', function() { | |||
| 
 | ||||
| // ************** Getting current state **************
 | ||||
| var getCurrentRoomList = function() { | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; | ||||
|     $.getJSON(url, function(data) { | ||||
|         for (var i=0; i<data.length; ++i) { | ||||
|             if ("messages" in data[i]) { | ||||
|                 data[i].latest_message = data[i].messages.chunk[0].content.body;    | ||||
|         var rooms = data.rooms; | ||||
|         for (var i=0; i<rooms.length; ++i) { | ||||
|             if ("messages" in rooms[i]) { | ||||
|                 rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;    | ||||
|             } | ||||
|         } | ||||
|         roomInfo = data; | ||||
|         roomInfo = rooms; | ||||
|         setRooms(roomInfo);   | ||||
|     }).fail(function(err) { | ||||
|         alert(JSON.stringify($.parseJSON(err.responseText))); | ||||
|  | @ -179,7 +181,8 @@ var loadRoomContent = function(roomId) { | |||
| 
 | ||||
| var getMessages = function(roomId) { | ||||
|     $("#messages").empty(); | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/messages/list?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=10"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +  | ||||
|               encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10"; | ||||
|     $.getJSON(url, function(data) { | ||||
|         for (var i=data.chunk.length-1; i>=0; --i) { | ||||
|             addMessage(data.chunk[i]);    | ||||
|  | @ -190,7 +193,8 @@ var getMessages = function(roomId) { | |||
| var getMemberList = function(roomId) { | ||||
|     $("#members").empty(); | ||||
|     memberInfo = []; | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/members/list?access_token=" + accountInfo.access_token; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +  | ||||
|               encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token; | ||||
|     $.getJSON(url, function(data) { | ||||
|         for (var i=0; i<data.chunk.length; ++i) { | ||||
|             memberInfo.push(data.chunk[i]); | ||||
|  | @ -212,11 +216,9 @@ $('.sendMessage').live('click', function() { | |||
| var sendMessage = function(roomId, body) { | ||||
|     var msgId = $.now(); | ||||
|      | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; | ||||
|     url = url.replace("$token", accountInfo.access_token); | ||||
|     url = url.replace("$roomid", encodeURIComponent(roomId)); | ||||
|     url = url.replace("$user", encodeURIComponent(accountInfo.user_id)); | ||||
|     url = url.replace("$msgid", msgId); | ||||
|      | ||||
|     var data = { | ||||
|         msgtype: "m.text", | ||||
|  | @ -225,7 +227,7 @@ var sendMessage = function(roomId, body) { | |||
|      | ||||
|     $.ajax({ | ||||
|         url: url, | ||||
|         type: "PUT", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify(data), | ||||
|         dataType: "json", | ||||
|  | @ -260,13 +262,12 @@ var setRooms = function(roomList) { | |||
|         var membership = $(this).find('td:eq(1)').text(); | ||||
|         if (membership !== "join") { | ||||
|             console.log("Joining room " + roomId);  | ||||
|             var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token"; | ||||
|             var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token"; | ||||
|             url = url.replace("$token", accountInfo.access_token); | ||||
|             url = url.replace("$roomid", encodeURIComponent(roomId)); | ||||
|             url = url.replace("$user", encodeURIComponent(accountInfo.user_id)); | ||||
|             $.ajax({ | ||||
|                 url: url, | ||||
|                 type: "PUT", | ||||
|                 type: "POST", | ||||
|                 contentType: "application/json; charset=utf-8", | ||||
|                 data: JSON.stringify({membership: "join"}), | ||||
|                 dataType: "json", | ||||
|  | @ -286,16 +287,33 @@ var setRooms = function(roomList) { | |||
| }; | ||||
| 
 | ||||
| var addMessage = function(data) { | ||||
| 
 | ||||
|     var msg = data.content.body; | ||||
|     if (data.type === "m.room.member") { | ||||
|         if (data.content.membership === "invite") { | ||||
|             msg = "<em>invited " + data.state_key + " to the room</em>"; | ||||
|         } | ||||
|         else if (data.content.membership === "join") { | ||||
|             msg = "<em>joined the room</em>"; | ||||
|         } | ||||
|         else if (data.content.membership === "leave") { | ||||
|             msg = "<em>left the room</em>"; | ||||
|         } | ||||
|         else { | ||||
|             msg = "<em>" + data.content.membership + "</em>"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     var row = "<tr>" + | ||||
|               "<td>"+data.user_id+"</td>" + | ||||
|               "<td>"+data.content.body+"</td>" + | ||||
|               "<td>"+msg+"</td>" + | ||||
|               "</tr>";  | ||||
|     $("#messages").append(row); | ||||
| }; | ||||
| 
 | ||||
| var addMember = function(data) { | ||||
|     var row = "<tr>" + | ||||
|               "<td>"+data.target_user_id+"</td>" + | ||||
|               "<td>"+data.state_key+"</td>" + | ||||
|               "<td>"+data.content.membership+"</td>" + | ||||
|               "</tr>";  | ||||
|     $("#members").append(row); | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ $('.register').live('click', function() { | |||
|     var user = $("#user").val(); | ||||
|     var password = $("#password").val(); | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/register", | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/register", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify({ user_id: user, password: password }), | ||||
|  | @ -27,7 +27,7 @@ $('.register').live('click', function() { | |||
| 
 | ||||
| var login = function(user, password) { | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/login", | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/login", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), | ||||
|  | @ -44,8 +44,8 @@ var login = function(user, password) { | |||
| $('.login').live('click', function() { | ||||
|     var user = $("#userLogin").val(); | ||||
|     var password = $("#passwordLogin").val(); | ||||
|     $.getJSON("http://localhost:8080/matrix/client/api/v1/login", function(data) { | ||||
|         if (data.type !== "m.login.password") { | ||||
|     $.getJSON("http://localhost:8080/_matrix/client/api/v1/login", function(data) { | ||||
|         if (data.flows[0].type !== "m.login.password") { | ||||
|             alert("I don't know how to login with this type: " + data.type); | ||||
|             return; | ||||
|         } | ||||
|  | @ -60,7 +60,7 @@ $('.logout').live('click', function() { | |||
| }); | ||||
| 
 | ||||
| $('.testToken').live('click', function() { | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; | ||||
|     $.getJSON(url, function(data) { | ||||
|          $("#imSyncText").text(JSON.stringify(data, undefined, 2)); | ||||
|     }).fail(function(err) { | ||||
|  |  | |||
|  | @ -14,9 +14,9 @@ | |||
|         <input type="text" id="roomId" placeholder="Room ID"></input> | ||||
|         <input type="text" id="targetUser" placeholder="Target User ID"></input> | ||||
|         <select id="membership"> | ||||
|             <option value="invite">Invite</option> | ||||
|             <option value="join">Join</option> | ||||
|             <option value="leave">Leave</option> | ||||
|             <option value="invite">invite</option> | ||||
|             <option value="join">join</option> | ||||
|             <option value="leave">leave</option> | ||||
|         </select> | ||||
|         <input type="button" class="changeMembership" value="Change Membership"></input> | ||||
|     </form> | ||||
|  |  | |||
|  | @ -4,13 +4,21 @@ var showLoggedIn = function(data) { | |||
|     accountInfo = data; | ||||
|     getCurrentRoomList(); | ||||
|     $(".loggedin").css({visibility: "visible"}); | ||||
|     $("#membership").change(function() { | ||||
|     if ($("#membership").val() === "invite") { | ||||
|         $("#targetUser").css({visibility: "visible"}); | ||||
|     } | ||||
|     else { | ||||
|         $("#targetUser").css({visibility: "hidden"}); | ||||
|     } | ||||
| }); | ||||
| }; | ||||
| 
 | ||||
| $('.login').live('click', function() { | ||||
|     var user = $("#userLogin").val(); | ||||
|     var password = $("#passwordLogin").val(); | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/login", | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/login", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), | ||||
|  | @ -31,10 +39,11 @@ var getCurrentRoomList = function() { | |||
|     // solution but that is out of scope of this fiddle.
 | ||||
|     $("#rooms").find("tr:gt(0)").remove(); | ||||
|      | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; | ||||
|     $.getJSON(url, function(data) { | ||||
|         for (var i=0; i<data.length; ++i) { | ||||
|             addRoom(data[i]);    | ||||
|         var rooms = data.rooms; | ||||
|         for (var i=0; i<rooms.length; ++i) { | ||||
|             addRoom(rooms[i]);    | ||||
|         } | ||||
|     }).fail(function(err) { | ||||
|         alert(JSON.stringify($.parseJSON(err.responseText))); | ||||
|  | @ -44,7 +53,7 @@ var getCurrentRoomList = function() { | |||
| $('.createRoom').live('click', function() { | ||||
|     var data = {}; | ||||
|     $.ajax({ | ||||
|         url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token, | ||||
|         url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify(data), | ||||
|  | @ -78,54 +87,42 @@ $('.changeMembership').live('click', function() { | |||
|         return; | ||||
|     } | ||||
|      | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token"; | ||||
|     url = url.replace("$token", accountInfo.access_token); | ||||
|     url = url.replace("$roomid", encodeURIComponent(roomId)); | ||||
|     url = url.replace("$user", encodeURIComponent(member)); | ||||
|     url = url.replace("$membership", membership); | ||||
|      | ||||
|     if (membership === "leave") { | ||||
|         $.ajax({ | ||||
|             url: url, | ||||
|             type: "DELETE", | ||||
|             contentType: "application/json; charset=utf-8", | ||||
|             dataType: "json", | ||||
|             success: function(data) { | ||||
|                 getCurrentRoomList(); | ||||
|             }, | ||||
|             error: function(err) { | ||||
|                 alert(JSON.stringify($.parseJSON(err.responseText)));   | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     else { | ||||
|         var data = { | ||||
|             membership: membership | ||||
|     var data = {}; | ||||
|      | ||||
|     if (membership === "invite") { | ||||
|         data = { | ||||
|             user_id: member | ||||
|         }; | ||||
|          | ||||
|         $.ajax({ | ||||
|             url: url, | ||||
|             type: "PUT", | ||||
|             contentType: "application/json; charset=utf-8", | ||||
|             data: JSON.stringify(data), | ||||
|             dataType: "json", | ||||
|             success: function(data) { | ||||
|                 getCurrentRoomList(); | ||||
|             }, | ||||
|             error: function(err) { | ||||
|                 alert(JSON.stringify($.parseJSON(err.responseText)));   | ||||
|             } | ||||
|         });  | ||||
|     } | ||||
| 
 | ||||
|     $.ajax({ | ||||
|         url: url, | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify(data), | ||||
|         dataType: "json", | ||||
|         success: function(data) { | ||||
|             getCurrentRoomList(); | ||||
|         }, | ||||
|         error: function(err) { | ||||
|             alert(JSON.stringify($.parseJSON(err.responseText)));   | ||||
|         } | ||||
|     });  | ||||
| }); | ||||
| 
 | ||||
| $('.joinAlias').live('click', function() { | ||||
|     var roomAlias = $("#roomAlias").val(); | ||||
|     var url = "http://localhost:8080/matrix/client/api/v1/join/$roomalias?access_token=$token"; | ||||
|     var url = "http://localhost:8080/_matrix/client/api/v1/join/$roomalias?access_token=$token"; | ||||
|     url = url.replace("$token", accountInfo.access_token); | ||||
|     url = url.replace("$roomalias", encodeURIComponent(roomAlias)); | ||||
|     $.ajax({ | ||||
|         url: url, | ||||
|         type: "PUT", | ||||
|         type: "POST", | ||||
|         contentType: "application/json; charset=utf-8", | ||||
|         data: JSON.stringify({}), | ||||
|         dataType: "json", | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ | |||
| 
 | ||||
| """Contains the URL paths to prefix various aspects of the server with. """ | ||||
| 
 | ||||
| CLIENT_PREFIX = "/matrix/client/api/v1" | ||||
| FEDERATION_PREFIX = "/matrix/federation/v1" | ||||
| WEB_CLIENT_PREFIX = "/matrix/client" | ||||
| CONTENT_REPO_PREFIX = "/matrix/content" | ||||
| CLIENT_PREFIX = "/_matrix/client/api/v1" | ||||
| FEDERATION_PREFIX = "/_matrix/federation/v1" | ||||
| WEB_CLIENT_PREFIX = "/_matrix/client" | ||||
| CONTENT_REPO_PREFIX = "/_matrix/content" | ||||
|  | @ -274,11 +274,11 @@ class MessageHandler(BaseRoomHandler): | |||
|                 messages, token = yield self.store.get_recent_events_for_room( | ||||
|                     event.room_id, | ||||
|                     limit=limit, | ||||
|                     end_token=now_token.events_key, | ||||
|                     end_token=now_token.room_key, | ||||
|                 ) | ||||
| 
 | ||||
|                 start_token = now_token.copy_and_replace("events_key", token[0]) | ||||
|                 end_token = now_token.copy_and_replace("events_key", token[1]) | ||||
|                 start_token = now_token.copy_and_replace("room_key", token[0]) | ||||
|                 end_token = now_token.copy_and_replace("room_key", token[1]) | ||||
| 
 | ||||
|                 d["messages"] = { | ||||
|                     "chunk": [m.get_dict() for m in messages], | ||||
|  |  | |||
|  | @ -260,19 +260,18 @@ class PresenceHandler(BaseHandler): | |||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def user_joined_room(self, user, room_id): | ||||
| 
 | ||||
|         if user.is_mine: | ||||
|             statuscache = self._get_or_make_usercache(user) | ||||
| 
 | ||||
|             # No actual update but we need to bump the serial anyway for the | ||||
|             # event source | ||||
|             self._user_cachemap_latest_serial += 1 | ||||
|             statuscache.update({}, serial=self._user_cachemap_latest_serial) | ||||
| 
 | ||||
|             self.push_update_to_local_and_remote( | ||||
|                 observed_user=user, | ||||
|                 room_ids=[room_id], | ||||
|                 statuscache=self._get_or_offline_usercache(user), | ||||
|             ) | ||||
| 
 | ||||
|         else: | ||||
|             self.push_update_to_clients( | ||||
|                 observed_user=user, | ||||
|                 room_ids=[room_id], | ||||
|                 statuscache=self._get_or_offline_usercache(user), | ||||
|                 statuscache=statuscache, | ||||
|             ) | ||||
| 
 | ||||
|         # We also want to tell them about current presence of people. | ||||
|  | @ -722,6 +721,78 @@ class PresenceHandler(BaseHandler): | |||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class PresenceEventSource(object): | ||||
|     def __init__(self, hs): | ||||
|         self.hs = hs | ||||
|         self.clock = hs.get_clock() | ||||
| 
 | ||||
|     def get_new_events_for_user(self, user, from_key, limit): | ||||
|         from_key = int(from_key) | ||||
| 
 | ||||
|         presence = self.hs.get_handlers().presence_handler | ||||
|         cachemap = presence._user_cachemap | ||||
| 
 | ||||
|         # TODO(paul): limit, and filter by visibility | ||||
|         updates = [(k, cachemap[k]) for k in cachemap | ||||
|                    if from_key < cachemap[k].serial] | ||||
| 
 | ||||
|         if updates: | ||||
|             clock = self.clock | ||||
| 
 | ||||
|             latest_serial = max([x[1].serial for x in updates]) | ||||
|             data = [x[1].make_event(user=x[0], clock=clock) for x in updates] | ||||
| 
 | ||||
|             return ((data, latest_serial)) | ||||
|         else: | ||||
|             return (([], presence._user_cachemap_latest_serial)) | ||||
| 
 | ||||
|     def get_current_key(self): | ||||
|         presence = self.hs.get_handlers().presence_handler | ||||
|         return presence._user_cachemap_latest_serial | ||||
| 
 | ||||
|     def get_pagination_rows(self, user, pagination_config, key): | ||||
|         # TODO (erikj): Does this make sense? Ordering? | ||||
| 
 | ||||
|         from_token = pagination_config.from_token | ||||
|         to_token = pagination_config.to_token | ||||
| 
 | ||||
|         from_key = int(from_token.presence_key) | ||||
| 
 | ||||
|         if to_token: | ||||
|             to_key = int(to_token.presence_key) | ||||
|         else: | ||||
|             to_key = -1 | ||||
| 
 | ||||
|         presence = self.hs.get_handlers().presence_handler | ||||
|         cachemap = presence._user_cachemap | ||||
| 
 | ||||
|         # TODO(paul): limit, and filter by visibility | ||||
|         updates = [(k, cachemap[k]) for k in cachemap | ||||
|                    if to_key < cachemap[k].serial < from_key] | ||||
| 
 | ||||
|         if updates: | ||||
|             clock = self.clock | ||||
| 
 | ||||
|             earliest_serial = max([x[1].serial for x in updates]) | ||||
|             data = [x[1].make_event(user=x[0], clock=clock) for x in updates] | ||||
| 
 | ||||
|             if to_token: | ||||
|                 next_token = to_token | ||||
|             else: | ||||
|                 next_token = from_token | ||||
| 
 | ||||
|             next_token = next_token.copy_and_replace( | ||||
|                 "presence_key", earliest_serial | ||||
|             ) | ||||
|             return ((data, next_token)) | ||||
|         else: | ||||
|             if not to_token: | ||||
|                 to_token = from_token.copy_and_replace( | ||||
|                     "presence_key", 0 | ||||
|                 ) | ||||
|             return (([], to_token)) | ||||
| 
 | ||||
| 
 | ||||
| class UserPresenceCache(object): | ||||
|     """Store an observed user's state and status message. | ||||
| 
 | ||||
|  |  | |||
|  | @ -497,3 +497,49 @@ class RoomListHandler(BaseRoomHandler): | |||
|         chunk = yield self.store.get_rooms(is_public=True) | ||||
|         # FIXME (erikj): START is no longer a valid value | ||||
|         defer.returnValue({"start": "START", "end": "END", "chunk": chunk}) | ||||
| 
 | ||||
| 
 | ||||
| class RoomEventSource(object): | ||||
|     def __init__(self, hs): | ||||
|         self.store = hs.get_datastore() | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def get_new_events_for_user(self, user, from_key, limit): | ||||
|         # We just ignore the key for now. | ||||
| 
 | ||||
|         to_key = yield self.get_current_key() | ||||
| 
 | ||||
|         events, end_key = yield self.store.get_room_events_stream( | ||||
|             user_id=user.to_string(), | ||||
|             from_key=from_key, | ||||
|             to_key=to_key, | ||||
|             room_id=None, | ||||
|             limit=limit, | ||||
|         ) | ||||
| 
 | ||||
|         defer.returnValue((events, end_key)) | ||||
| 
 | ||||
|     def get_current_key(self): | ||||
|         return self.store.get_room_events_max_id() | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def get_pagination_rows(self, user, pagination_config, key): | ||||
|         from_token = pagination_config.from_token | ||||
|         to_token = pagination_config.to_token | ||||
|         limit = pagination_config.limit | ||||
|         direction = pagination_config.direction | ||||
| 
 | ||||
|         to_key = to_token.room_key if to_token else None | ||||
| 
 | ||||
|         events, next_key = yield self.store.paginate_room_events( | ||||
|             room_id=key, | ||||
|             from_key=from_token.room_key, | ||||
|             to_key=to_key, | ||||
|             direction=direction, | ||||
|             limit=limit, | ||||
|             with_feedback=True | ||||
|         ) | ||||
| 
 | ||||
|         next_token = from_token.copy_and_replace("room_key", next_key) | ||||
| 
 | ||||
|         defer.returnValue((events, next_token)) | ||||
|  |  | |||
|  | @ -145,3 +145,17 @@ class TypingNotificationHandler(BaseHandler): | |||
|             typing): | ||||
|         # TODO(paul) steal this from presence.py | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class TypingNotificationEventSource(object): | ||||
|     def __init__(self, hs): | ||||
|         self.hs = hs | ||||
| 
 | ||||
|     def get_new_events_for_user(self, user, from_key, limit): | ||||
|         return ([], from_key) | ||||
| 
 | ||||
|     def get_current_key(self): | ||||
|         return 0 | ||||
| 
 | ||||
|     def get_pagination_rows(self, user, pagination_config, key): | ||||
|         return ([], pagination_config.from_token) | ||||
|  |  | |||
|  | @ -325,7 +325,7 @@ class ContentRepoResource(resource.Resource): | |||
| 
 | ||||
|             # FIXME (erikj): These should use constants. | ||||
|             file_name = os.path.basename(fname) | ||||
|             url = "http://%s/matrix/content/%s" % ( | ||||
|             url = "http://%s/_matrix/content/%s" % ( | ||||
|                 self.hs.domain_with_port, file_name | ||||
|             ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -95,7 +95,7 @@ class Notifier(object): | |||
|         """ | ||||
|         room_id = event.room_id | ||||
| 
 | ||||
|         source = self.event_sources.sources["room"] | ||||
|         room_source = self.event_sources.sources["room"] | ||||
| 
 | ||||
|         listeners = self.rooms_to_listeners.get(room_id, set()).copy() | ||||
| 
 | ||||
|  | @ -107,13 +107,17 @@ class Notifier(object): | |||
|         # TODO (erikj): Can we make this more efficient by hitting the | ||||
|         # db once? | ||||
|         for listener in listeners: | ||||
|             events, end_token = yield source.get_new_events_for_user( | ||||
|             events, end_key = yield room_source.get_new_events_for_user( | ||||
|                 listener.user, | ||||
|                 listener.from_token, | ||||
|                 listener.from_token.room_key, | ||||
|                 listener.limit, | ||||
|             ) | ||||
| 
 | ||||
|             if events: | ||||
|                 end_token = listener.from_token.copy_and_replace( | ||||
|                     "room_key", end_key | ||||
|                 ) | ||||
| 
 | ||||
|                 listener.notify( | ||||
|                     self, events, listener.from_token, end_token | ||||
|                 ) | ||||
|  | @ -126,7 +130,7 @@ class Notifier(object): | |||
| 
 | ||||
|         Will wake up all listeners for the given users and rooms. | ||||
|         """ | ||||
|         source = self.event_sources.sources["presence"] | ||||
|         presence_source = self.event_sources.sources["presence"] | ||||
| 
 | ||||
|         listeners = set() | ||||
| 
 | ||||
|  | @ -137,13 +141,17 @@ class Notifier(object): | |||
|             listeners |= self.rooms_to_listeners.get(room, set()).copy() | ||||
| 
 | ||||
|         for listener in listeners: | ||||
|             events, end_token = yield source.get_new_events_for_user( | ||||
|             events, end_key = yield presence_source.get_new_events_for_user( | ||||
|                 listener.user, | ||||
|                 listener.from_token, | ||||
|                 listener.from_token.presence_key, | ||||
|                 listener.limit, | ||||
|             ) | ||||
| 
 | ||||
|             if events: | ||||
|                 end_token = listener.from_token.copy_and_replace( | ||||
|                     "presence_key", end_key | ||||
|                 ) | ||||
| 
 | ||||
|                 listener.notify( | ||||
|                     self, events, listener.from_token, end_token | ||||
|                 ) | ||||
|  | @ -216,16 +224,18 @@ class Notifier(object): | |||
|         limit = listener.limit | ||||
| 
 | ||||
|         # TODO (erikj): DeferredList? | ||||
|         for source in self.event_sources.sources.values(): | ||||
|             stuff, new_token = yield source.get_new_events_for_user( | ||||
|         for name, source in self.event_sources.sources.items(): | ||||
|             keyname = "%s_key" % name | ||||
| 
 | ||||
|             stuff, new_key = yield source.get_new_events_for_user( | ||||
|                 listener.user, | ||||
|                 from_token, | ||||
|                 getattr(from_token, keyname), | ||||
|                 limit, | ||||
|             ) | ||||
| 
 | ||||
|             events.extend(stuff) | ||||
| 
 | ||||
|             from_token = new_token | ||||
|             from_token = from_token.copy_and_replace(keyname, new_key) | ||||
| 
 | ||||
|         end_token = from_token | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,6 +17,10 @@ from twisted.internet import defer | |||
| 
 | ||||
| from synapse.types import StreamToken | ||||
| 
 | ||||
| from synapse.handlers.presence import PresenceEventSource | ||||
| from synapse.handlers.room import RoomEventSource | ||||
| from synapse.handlers.typing import TypingNotificationEventSource | ||||
| 
 | ||||
| 
 | ||||
| class NullSource(object): | ||||
|     """This event source never yields any events and its token remains at | ||||
|  | @ -24,146 +28,21 @@ class NullSource(object): | |||
|     def __init__(self, hs): | ||||
|         pass | ||||
| 
 | ||||
|     def get_new_events_for_user(self, user, from_token, limit): | ||||
|         return defer.succeed(([], from_token)) | ||||
|     def get_new_events_for_user(self, user, from_key, limit): | ||||
|         return defer.succeed(([], from_key)) | ||||
| 
 | ||||
|     def get_current_token_part(self): | ||||
|     def get_current_key(self): | ||||
|         return defer.succeed(0) | ||||
| 
 | ||||
|     def get_pagination_rows(self, user, pagination_config, key): | ||||
|         return defer.succeed(([], pagination_config.from_token)) | ||||
| 
 | ||||
| 
 | ||||
| class RoomEventSource(object): | ||||
|     def __init__(self, hs): | ||||
|         self.store = hs.get_datastore() | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def get_new_events_for_user(self, user, from_token, limit): | ||||
|         # We just ignore the key for now. | ||||
| 
 | ||||
|         to_key = yield self.get_current_token_part() | ||||
| 
 | ||||
|         events, end_key = yield self.store.get_room_events_stream( | ||||
|             user_id=user.to_string(), | ||||
|             from_key=from_token.events_key, | ||||
|             to_key=to_key, | ||||
|             room_id=None, | ||||
|             limit=limit, | ||||
|         ) | ||||
| 
 | ||||
|         end_token = from_token.copy_and_replace("events_key", end_key) | ||||
| 
 | ||||
|         defer.returnValue((events, end_token)) | ||||
| 
 | ||||
|     def get_current_token_part(self): | ||||
|         return self.store.get_room_events_max_id() | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def get_pagination_rows(self, user, pagination_config, key): | ||||
|         from_token = pagination_config.from_token | ||||
|         to_token = pagination_config.to_token | ||||
|         limit = pagination_config.limit | ||||
|         direction = pagination_config.direction | ||||
| 
 | ||||
|         to_key = to_token.events_key if to_token else None | ||||
| 
 | ||||
|         events, next_key = yield self.store.paginate_room_events( | ||||
|             room_id=key, | ||||
|             from_key=from_token.events_key, | ||||
|             to_key=to_key, | ||||
|             direction=direction, | ||||
|             limit=limit, | ||||
|             with_feedback=True | ||||
|         ) | ||||
| 
 | ||||
|         next_token = from_token.copy_and_replace("events_key", next_key) | ||||
| 
 | ||||
|         defer.returnValue((events, next_token)) | ||||
| 
 | ||||
| 
 | ||||
| class PresenceSource(object): | ||||
|     def __init__(self, hs): | ||||
|         self.hs = hs | ||||
|         self.clock = hs.get_clock() | ||||
| 
 | ||||
|     def get_new_events_for_user(self, user, from_token, limit): | ||||
|         from_key = int(from_token.presence_key) | ||||
| 
 | ||||
|         presence = self.hs.get_handlers().presence_handler | ||||
|         cachemap = presence._user_cachemap | ||||
| 
 | ||||
|         # TODO(paul): limit, and filter by visibility | ||||
|         updates = [(k, cachemap[k]) for k in cachemap | ||||
|                    if from_key < cachemap[k].serial] | ||||
| 
 | ||||
|         if updates: | ||||
|             clock = self.clock | ||||
| 
 | ||||
|             latest_serial = max([x[1].serial for x in updates]) | ||||
|             data = [x[1].make_event(user=x[0], clock=clock) for x in updates] | ||||
| 
 | ||||
|             end_token = from_token.copy_and_replace( | ||||
|                 "presence_key", latest_serial | ||||
|             ) | ||||
|             return ((data, end_token)) | ||||
|         else: | ||||
|             end_token = from_token.copy_and_replace( | ||||
|                 "presence_key", presence._user_cachemap_latest_serial | ||||
|             ) | ||||
|             return (([], end_token)) | ||||
| 
 | ||||
|     def get_current_token_part(self): | ||||
|         presence = self.hs.get_handlers().presence_handler | ||||
|         return presence._user_cachemap_latest_serial | ||||
| 
 | ||||
|     def get_pagination_rows(self, user, pagination_config, key): | ||||
|         # TODO (erikj): Does this make sense? Ordering? | ||||
| 
 | ||||
|         from_token = pagination_config.from_token | ||||
|         to_token = pagination_config.to_token | ||||
| 
 | ||||
|         from_key = int(from_token.presence_key) | ||||
| 
 | ||||
|         if to_token: | ||||
|             to_key = int(to_token.presence_key) | ||||
|         else: | ||||
|             to_key = -1 | ||||
| 
 | ||||
|         presence = self.hs.get_handlers().presence_handler | ||||
|         cachemap = presence._user_cachemap | ||||
| 
 | ||||
|         # TODO(paul): limit, and filter by visibility | ||||
|         updates = [(k, cachemap[k]) for k in cachemap | ||||
|                    if to_key < cachemap[k].serial < from_key] | ||||
| 
 | ||||
|         if updates: | ||||
|             clock = self.clock | ||||
| 
 | ||||
|             earliest_serial = max([x[1].serial for x in updates]) | ||||
|             data = [x[1].make_event(user=x[0], clock=clock) for x in updates] | ||||
| 
 | ||||
|             if to_token: | ||||
|                 next_token = to_token | ||||
|             else: | ||||
|                 next_token = from_token | ||||
| 
 | ||||
|             next_token = next_token.copy_and_replace( | ||||
|                 "presence_key", earliest_serial | ||||
|             ) | ||||
|             return ((data, next_token)) | ||||
|         else: | ||||
|             if not to_token: | ||||
|                 to_token = from_token.copy_and_replace( | ||||
|                     "presence_key", 0 | ||||
|                 ) | ||||
|             return (([], to_token)) | ||||
| 
 | ||||
| 
 | ||||
| class EventSources(object): | ||||
|     SOURCE_TYPES = { | ||||
|         "room": RoomEventSource, | ||||
|         "presence": PresenceSource, | ||||
|         "presence": PresenceEventSource, | ||||
|         "typing": TypingNotificationEventSource, | ||||
|     } | ||||
| 
 | ||||
|     def __init__(self, hs): | ||||
|  | @ -172,24 +51,29 @@ class EventSources(object): | |||
|             for name, cls in EventSources.SOURCE_TYPES.items() | ||||
|         } | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def create_token(events_key, presence_key): | ||||
|         return StreamToken(events_key=events_key, presence_key=presence_key) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def get_current_token(self): | ||||
|         events_key = yield self.sources["room"].get_current_token_part() | ||||
|         presence_key = yield self.sources["presence"].get_current_token_part() | ||||
|         token = EventSources.create_token(events_key, presence_key) | ||||
|         token = StreamToken( | ||||
|             room_key=( | ||||
|                 yield self.sources["room"].get_current_key() | ||||
|             ), | ||||
|             presence_key=( | ||||
|                 yield self.sources["presence"].get_current_key() | ||||
|             ), | ||||
|             typing_key=( | ||||
|                 yield self.sources["typing"].get_current_key() | ||||
|             ) | ||||
|         ) | ||||
|         defer.returnValue(token) | ||||
| 
 | ||||
| 
 | ||||
| class StreamSource(object): | ||||
|     def get_new_events_for_user(self, user, from_token, limit): | ||||
|     def get_new_events_for_user(self, user, from_key, limit): | ||||
|         """from_key is the key within this event source.""" | ||||
|         raise NotImplementedError("get_new_events_for_user") | ||||
| 
 | ||||
|     def get_current_token_part(self): | ||||
|         raise NotImplementedError("get_current_token_part") | ||||
|     def get_current_key(self): | ||||
|         raise NotImplementedError("get_current_key") | ||||
| 
 | ||||
|     def get_pagination_rows(self, user, pagination_config, key): | ||||
|         raise NotImplementedError("get_rows") | ||||
|  |  | |||
|  | @ -97,7 +97,7 @@ class RoomID(DomainSpecificString): | |||
| class StreamToken( | ||||
|     namedtuple( | ||||
|         "Token", | ||||
|         ("events_key", "presence_key") | ||||
|         ("room_key", "presence_key", "typing_key") | ||||
|     ) | ||||
| ): | ||||
|     _SEPARATOR = "_" | ||||
|  | @ -105,21 +105,14 @@ class StreamToken( | |||
|     @classmethod | ||||
|     def from_string(cls, string): | ||||
|         try: | ||||
|             events_key, presence_key = string.split(cls._SEPARATOR) | ||||
|             keys = string.split(cls._SEPARATOR) | ||||
| 
 | ||||
|             return cls( | ||||
|                 events_key=events_key, | ||||
|                 presence_key=presence_key, | ||||
|             ) | ||||
|             return cls(*keys) | ||||
|         except: | ||||
|             raise SynapseError(400, "Invalid Token") | ||||
| 
 | ||||
|     def to_string(self): | ||||
|         return "".join([ | ||||
|             str(self.events_key), | ||||
|             self._SEPARATOR, | ||||
|             str(self.presence_key), | ||||
|         ]) | ||||
|         return self._SEPARATOR.join([str(k) for k in self]) | ||||
| 
 | ||||
|     def copy_and_replace(self, key, new_value): | ||||
|         d = self._asdict() | ||||
|  |  | |||
|  | @ -87,7 +87,7 @@ class FederationTestCase(unittest.TestCase): | |||
| 
 | ||||
|         # Empty context initially | ||||
|         (code, response) = yield self.mock_resource.trigger("GET", | ||||
|                 "/matrix/federation/v1/state/my-context/", None) | ||||
|                 "/_matrix/federation/v1/state/my-context/", None) | ||||
|         self.assertEquals(200, code) | ||||
|         self.assertFalse(response["pdus"]) | ||||
| 
 | ||||
|  | @ -112,7 +112,7 @@ class FederationTestCase(unittest.TestCase): | |||
|         ) | ||||
| 
 | ||||
|         (code, response) = yield self.mock_resource.trigger("GET", | ||||
|                 "/matrix/federation/v1/state/my-context/", None) | ||||
|                 "/_matrix/federation/v1/state/my-context/", None) | ||||
|         self.assertEquals(200, code) | ||||
|         self.assertEquals(1, len(response["pdus"])) | ||||
| 
 | ||||
|  | @ -123,7 +123,7 @@ class FederationTestCase(unittest.TestCase): | |||
|         ) | ||||
| 
 | ||||
|         (code, response) = yield self.mock_resource.trigger("GET", | ||||
|                 "/matrix/federation/v1/pdu/red/abc123def456/", None) | ||||
|                 "/_matrix/federation/v1/pdu/red/abc123def456/", None) | ||||
|         self.assertEquals(404, code) | ||||
| 
 | ||||
|         # Now insert such a PDU | ||||
|  | @ -142,7 +142,7 @@ class FederationTestCase(unittest.TestCase): | |||
|         ) | ||||
| 
 | ||||
|         (code, response) = yield self.mock_resource.trigger("GET", | ||||
|                 "/matrix/federation/v1/pdu/red/abc123def456/", None) | ||||
|                 "/_matrix/federation/v1/pdu/red/abc123def456/", None) | ||||
|         self.assertEquals(200, code) | ||||
|         self.assertEquals(1, len(response["pdus"])) | ||||
|         self.assertEquals("m.text", response["pdus"][0]["pdu_type"]) | ||||
|  | @ -168,7 +168,7 @@ class FederationTestCase(unittest.TestCase): | |||
| 
 | ||||
|         self.mock_http_client.put_json.assert_called_with( | ||||
|                 "remote", | ||||
|                 path="/matrix/federation/v1/send/1000000/", | ||||
|                 path="/_matrix/federation/v1/send/1000000/", | ||||
|                 data={ | ||||
|                     "ts": 1000000, | ||||
|                     "origin": "test", | ||||
|  | @ -203,7 +203,7 @@ class FederationTestCase(unittest.TestCase): | |||
|         # MockClock ensures we can guess these timestamps | ||||
|         self.mock_http_client.put_json.assert_called_with( | ||||
|                 "remote", | ||||
|                 path="/matrix/federation/v1/send/1000000/", | ||||
|                 path="/_matrix/federation/v1/send/1000000/", | ||||
|                 data={ | ||||
|                     "origin": "test", | ||||
|                     "ts": 1000000, | ||||
|  | @ -226,7 +226,7 @@ class FederationTestCase(unittest.TestCase): | |||
|         self.federation.register_edu_handler("m.test", recv_observer) | ||||
| 
 | ||||
|         yield self.mock_resource.trigger("PUT", | ||||
|                 "/matrix/federation/v1/send/1001000/", | ||||
|                 "/_matrix/federation/v1/send/1001000/", | ||||
|                 """{ | ||||
|                     "origin": "remote", | ||||
|                     "ts": 1001000, | ||||
|  | @ -261,7 +261,7 @@ class FederationTestCase(unittest.TestCase): | |||
| 
 | ||||
|         self.mock_http_client.get_json.assert_called_with( | ||||
|             destination="remote", | ||||
|             path="/matrix/federation/v1/query/a-question", | ||||
|             path="/_matrix/federation/v1/query/a-question", | ||||
|             args={"one": "1", "two": "2"} | ||||
|         ) | ||||
| 
 | ||||
|  | @ -273,7 +273,7 @@ class FederationTestCase(unittest.TestCase): | |||
|         self.federation.register_query_handler("a-question", recv_handler) | ||||
| 
 | ||||
|         code, response = yield self.mock_resource.trigger("GET", | ||||
|             "/matrix/federation/v1/query/a-question?three=3&four=4", None) | ||||
|             "/_matrix/federation/v1/query/a-question?three=3&four=4", None) | ||||
| 
 | ||||
|         self.assertEquals(200, code) | ||||
|         self.assertEquals({"another": "response"}, response) | ||||
|  |  | |||
|  | @ -314,7 +314,7 @@ class PresenceInvitesTestCase(unittest.TestCase): | |||
|         put_json = self.mock_http_client.put_json | ||||
|         put_json.expect_call_and_return( | ||||
|             call("elsewhere", | ||||
|                 path="/matrix/federation/v1/send/1000000/", | ||||
|                 path="/_matrix/federation/v1/send/1000000/", | ||||
|                 data=_expect_edu("elsewhere", "m.presence_invite", | ||||
|                     content={ | ||||
|                         "observer_user": "@apple:test", | ||||
|  | @ -340,7 +340,7 @@ class PresenceInvitesTestCase(unittest.TestCase): | |||
|         put_json = self.mock_http_client.put_json | ||||
|         put_json.expect_call_and_return( | ||||
|             call("elsewhere", | ||||
|                 path="/matrix/federation/v1/send/1000000/", | ||||
|                 path="/_matrix/federation/v1/send/1000000/", | ||||
|                 data=_expect_edu("elsewhere", "m.presence_accept", | ||||
|                     content={ | ||||
|                         "observer_user": "@cabbage:elsewhere", | ||||
|  | @ -352,7 +352,7 @@ class PresenceInvitesTestCase(unittest.TestCase): | |||
|         ) | ||||
| 
 | ||||
|         yield self.mock_federation_resource.trigger("PUT", | ||||
|             "/matrix/federation/v1/send/1000000/", | ||||
|             "/_matrix/federation/v1/send/1000000/", | ||||
|             _make_edu_json("elsewhere", "m.presence_invite", | ||||
|                 content={ | ||||
|                     "observer_user": "@cabbage:elsewhere", | ||||
|  | @ -371,7 +371,7 @@ class PresenceInvitesTestCase(unittest.TestCase): | |||
|         put_json = self.mock_http_client.put_json | ||||
|         put_json.expect_call_and_return( | ||||
|             call("elsewhere", | ||||
|                 path="/matrix/federation/v1/send/1000000/", | ||||
|                 path="/_matrix/federation/v1/send/1000000/", | ||||
|                 data=_expect_edu("elsewhere", "m.presence_deny", | ||||
|                     content={ | ||||
|                         "observer_user": "@cabbage:elsewhere", | ||||
|  | @ -383,7 +383,7 @@ class PresenceInvitesTestCase(unittest.TestCase): | |||
|         ) | ||||
| 
 | ||||
|         yield self.mock_federation_resource.trigger("PUT", | ||||
|             "/matrix/federation/v1/send/1000000/", | ||||
|             "/_matrix/federation/v1/send/1000000/", | ||||
|             _make_edu_json("elsewhere", "m.presence_invite", | ||||
|                 content={ | ||||
|                     "observer_user": "@cabbage:elsewhere", | ||||
|  | @ -397,7 +397,7 @@ class PresenceInvitesTestCase(unittest.TestCase): | |||
|     @defer.inlineCallbacks | ||||
|     def test_accepted_remote(self): | ||||
|         yield self.mock_federation_resource.trigger("PUT", | ||||
|             "/matrix/federation/v1/send/1000000/", | ||||
|             "/_matrix/federation/v1/send/1000000/", | ||||
|             _make_edu_json("elsewhere", "m.presence_accept", | ||||
|                 content={ | ||||
|                     "observer_user": "@apple:test", | ||||
|  | @ -415,7 +415,7 @@ class PresenceInvitesTestCase(unittest.TestCase): | |||
|     @defer.inlineCallbacks | ||||
|     def test_denied_remote(self): | ||||
|         yield self.mock_federation_resource.trigger("PUT", | ||||
|             "/matrix/federation/v1/send/1000000/", | ||||
|             "/_matrix/federation/v1/send/1000000/", | ||||
|             _make_edu_json("elsewhere", "m.presence_deny", | ||||
|                 content={ | ||||
|                     "observer_user": "@apple:test", | ||||
|  | @ -514,13 +514,6 @@ class PresencePushTestCase(unittest.TestCase): | |||
|             ) | ||||
|         hs.handlers = JustPresenceHandlers(hs) | ||||
| 
 | ||||
|         def update(*args,**kwargs): | ||||
|             # print "mock_update_client: Args=%s, kwargs=%s" %(args, kwargs,) | ||||
|             return defer.succeed(None) | ||||
| 
 | ||||
|         self.mock_update_client = Mock() | ||||
|         self.mock_update_client.side_effect = update | ||||
| 
 | ||||
|         self.datastore = hs.get_datastore() | ||||
| 
 | ||||
|         def get_received_txn_response(*args): | ||||
|  | @ -528,7 +521,7 @@ class PresencePushTestCase(unittest.TestCase): | |||
|         self.datastore.get_received_txn_response = get_received_txn_response | ||||
| 
 | ||||
|         self.handler = hs.get_handlers().presence_handler | ||||
|         self.handler.push_update_to_clients = self.mock_update_client | ||||
|         self.event_source = hs.get_event_sources().sources["presence"] | ||||
| 
 | ||||
|         # Mock the RoomMemberHandler | ||||
|         hs.handlers.room_member_handler = Mock(spec=[ | ||||
|  | @ -622,16 +615,23 @@ class PresencePushTestCase(unittest.TestCase): | |||
|         apple_set.add(self.u_banana) | ||||
|         apple_set.add(self.u_clementine) | ||||
| 
 | ||||
|         self.assertEquals(self.event_source.get_current_key(), 0) | ||||
| 
 | ||||
|         yield self.handler.set_state(self.u_apple, self.u_apple, | ||||
|                 {"state": ONLINE}) | ||||
| 
 | ||||
|         self.mock_update_client.assert_has_calls([ | ||||
|                 call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]), | ||||
|                     room_ids=["a-room"], | ||||
|                     observed_user=self.u_apple, | ||||
|                     statuscache=ANY), # self-reflection | ||||
|         ], any_order=True) | ||||
|         self.mock_update_client.reset_mock() | ||||
|         self.assertEquals(self.event_source.get_current_key(), 1) | ||||
|         self.assertEquals( | ||||
|             self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0], | ||||
|             [ | ||||
|                 {"type": "m.presence", | ||||
|                  "content": { | ||||
|                     "user_id": "@apple:test", | ||||
|                     "state": ONLINE, | ||||
|                     "mtime_age": 0, | ||||
|                 }}, | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|         presence = yield self.handler.get_presence_list( | ||||
|                 observer_user=self.u_apple, accepted=True) | ||||
|  | @ -657,31 +657,24 @@ class PresencePushTestCase(unittest.TestCase): | |||
|                  "state": OFFLINE}, | ||||
|         ], presence) | ||||
| 
 | ||||
|         self.mock_update_client.assert_has_calls([ | ||||
|                 call(users_to_push=set([self.u_banana]), | ||||
|                     room_ids=[], | ||||
|                     observed_user=self.u_banana, | ||||
|                     statuscache=ANY), # self-reflection | ||||
|         ]) # and no others... | ||||
|         self.assertEquals(self.event_source.get_current_key(), 2) | ||||
|         self.assertEquals( | ||||
|             self.event_source.get_new_events_for_user( | ||||
|                 self.u_banana, 1, None | ||||
|             )[0], | ||||
|             [ | ||||
|                 {"type": "m.presence", | ||||
|                  "content": { | ||||
|                      "user_id": "@banana:test", | ||||
|                      "state": ONLINE, | ||||
|                      "mtime_age": 2000 | ||||
|                 }}, | ||||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def test_push_remote(self): | ||||
|         put_json = self.mock_http_client.put_json | ||||
| #        put_json.expect_call_and_return( | ||||
| #            call("remote", | ||||
| #                path=ANY,  # Can't guarantee which txn ID will be which | ||||
| #                data=_expect_edu("remote", "m.presence", | ||||
| #                    content={ | ||||
| #                        "push": [ | ||||
| #                            {"user_id": "@apple:test", | ||||
| #                             "state": "online", | ||||
| #                             "mtime_age": 0}, | ||||
| #                        ], | ||||
| #                    } | ||||
| #                ) | ||||
| #            ), | ||||
| #            defer.succeed((200, "OK")) | ||||
| #        ) | ||||
|         put_json.expect_call_and_return( | ||||
|             call("farm", | ||||
|                 path=ANY,  # Can't guarantee which txn ID will be which | ||||
|  | @ -724,8 +717,10 @@ class PresencePushTestCase(unittest.TestCase): | |||
| 
 | ||||
|         self.room_members = [self.u_banana, self.u_potato] | ||||
| 
 | ||||
|         self.assertEquals(self.event_source.get_current_key(), 0) | ||||
| 
 | ||||
|         yield self.mock_federation_resource.trigger("PUT", | ||||
|             "/matrix/federation/v1/send/1000000/", | ||||
|             "/_matrix/federation/v1/send/1000000/", | ||||
|             _make_edu_json("elsewhere", "m.presence", | ||||
|                 content={ | ||||
|                     "push": [ | ||||
|  | @ -737,12 +732,20 @@ class PresencePushTestCase(unittest.TestCase): | |||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         self.mock_update_client.assert_has_calls([ | ||||
|                 call(users_to_push=set([self.u_apple]), | ||||
|                     room_ids=["a-room"], | ||||
|                     observed_user=self.u_potato, | ||||
|                     statuscache=ANY), | ||||
|         ], any_order=True) | ||||
|         self.assertEquals(self.event_source.get_current_key(), 1) | ||||
|         self.assertEquals( | ||||
|             self.event_source.get_new_events_for_user( | ||||
|                 self.u_apple, 0, None | ||||
|             )[0], | ||||
|             [ | ||||
|                 {"type": "m.presence", | ||||
|                  "content": { | ||||
|                      "user_id": "@potato:remote", | ||||
|                      "state": ONLINE, | ||||
|                      "mtime_age": 1000, | ||||
|                 }} | ||||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|         self.clock.advance_time(2) | ||||
| 
 | ||||
|  | @ -754,24 +757,35 @@ class PresencePushTestCase(unittest.TestCase): | |||
|     def test_join_room_local(self): | ||||
|         self.room_members = [self.u_apple, self.u_banana] | ||||
| 
 | ||||
|         yield self.distributor.fire("user_joined_room", self.u_elderberry, | ||||
|         self.assertEquals(self.event_source.get_current_key(), 0) | ||||
| 
 | ||||
|         # TODO(paul): Gut-wrenching | ||||
|         self.handler._user_cachemap[self.u_clementine] = UserPresenceCache() | ||||
|         self.handler._user_cachemap[self.u_clementine].update( | ||||
|             { | ||||
|                 "state": PresenceState.ONLINE, | ||||
|                 "mtime": self.clock.time_msec(), | ||||
|             }, self.u_clementine | ||||
|         ) | ||||
| 
 | ||||
|         yield self.distributor.fire("user_joined_room", self.u_clementine, | ||||
|             "a-room" | ||||
|         ) | ||||
| 
 | ||||
|         self.mock_update_client.assert_has_calls([ | ||||
|             call(room_ids=["a-room"], | ||||
|                 observed_user=self.u_elderberry, | ||||
|                 users_to_push=set(), | ||||
|                 statuscache=ANY), | ||||
|             call(users_to_push=set([self.u_elderberry]), | ||||
|                 observed_user=self.u_apple, | ||||
|                 room_ids=[], | ||||
|                 statuscache=ANY), | ||||
|             call(users_to_push=set([self.u_elderberry]), | ||||
|                 observed_user=self.u_banana, | ||||
|                 room_ids=[], | ||||
|                 statuscache=ANY), | ||||
|         ], any_order=True) | ||||
|         self.assertEquals(self.event_source.get_current_key(), 1) | ||||
|         self.assertEquals( | ||||
|             self.event_source.get_new_events_for_user( | ||||
|                 self.u_apple, 0, None | ||||
|             )[0], | ||||
|             [ | ||||
|                 {"type": "m.presence", | ||||
|                  "content": { | ||||
|                      "user_id": "@clementine:test", | ||||
|                      "state": ONLINE, | ||||
|                      "mtime_age": 0, | ||||
|                 }} | ||||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def test_join_room_remote(self): | ||||
|  | @ -822,7 +836,7 @@ class PresencePushTestCase(unittest.TestCase): | |||
| 
 | ||||
|         put_json.expect_call_and_return( | ||||
|             call("remote", | ||||
|                 path="/matrix/federation/v1/send/1000002/", | ||||
|                 path="/_matrix/federation/v1/send/1000002/", | ||||
|                 data=_expect_edu("remote", "m.presence", | ||||
|                     content={ | ||||
|                         "push": [ | ||||
|  | @ -1116,7 +1130,7 @@ class PresencePollingTestCase(unittest.TestCase): | |||
|         put_json = self.mock_http_client.put_json | ||||
|         put_json.expect_call_and_return( | ||||
|             call("remote", | ||||
|                 path="/matrix/federation/v1/send/1000000/", | ||||
|                 path="/_matrix/federation/v1/send/1000000/", | ||||
|                 data=_expect_edu("remote", "m.presence", | ||||
|                     content={ | ||||
|                         "push": [ | ||||
|  | @ -1131,7 +1145,7 @@ class PresencePollingTestCase(unittest.TestCase): | |||
|         ) | ||||
| 
 | ||||
|         yield self.mock_federation_resource.trigger("PUT", | ||||
|             "/matrix/federation/v1/send/1000000/", | ||||
|             "/_matrix/federation/v1/send/1000000/", | ||||
|             _make_edu_json("remote", "m.presence", | ||||
|                 content={ | ||||
|                     "poll": [ "@banana:test" ], | ||||
|  | @ -1145,7 +1159,7 @@ class PresencePollingTestCase(unittest.TestCase): | |||
|         self.assertTrue(self.u_banana in self.handler._remote_sendmap) | ||||
| 
 | ||||
|         yield self.mock_federation_resource.trigger("PUT", | ||||
|             "/matrix/federation/v1/send/1000001/", | ||||
|             "/_matrix/federation/v1/send/1000001/", | ||||
|             _make_edu_json("remote", "m.presence", | ||||
|                 content={ | ||||
|                     "unpoll": [ "@banana:test" ], | ||||
|  |  | |||
|  | @ -166,7 +166,7 @@ class TypingNotificationsTestCase(unittest.TestCase): | |||
|         put_json = self.mock_http_client.put_json | ||||
|         put_json.expect_call_and_return( | ||||
|             call("farm", | ||||
|                 path="/matrix/federation/v1/send/1000000/", | ||||
|                 path="/_matrix/federation/v1/send/1000000/", | ||||
|                 data=_expect_edu("farm", "m.typing", | ||||
|                     content={ | ||||
|                         "room_id": self.room_id, | ||||
|  | @ -192,7 +192,7 @@ class TypingNotificationsTestCase(unittest.TestCase): | |||
|         self.room_members = [self.u_apple, self.u_onion] | ||||
| 
 | ||||
|         yield self.mock_federation_resource.trigger("PUT", | ||||
|             "/matrix/federation/v1/send/1000000/", | ||||
|             "/_matrix/federation/v1/send/1000000/", | ||||
|             _make_edu_json("farm", "m.typing", | ||||
|                 content={ | ||||
|                     "room_id": self.room_id, | ||||
|  | @ -216,7 +216,7 @@ class TypingNotificationsTestCase(unittest.TestCase): | |||
|         put_json = self.mock_http_client.put_json | ||||
|         put_json.expect_call_and_return( | ||||
|             call("farm", | ||||
|                 path="/matrix/federation/v1/send/1000000/", | ||||
|                 path="/_matrix/federation/v1/send/1000000/", | ||||
|                 data=_expect_edu("farm", "m.typing", | ||||
|                     content={ | ||||
|                         "room_id": self.room_id, | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ from mock import Mock | |||
| 
 | ||||
| logging.getLogger().addHandler(logging.NullHandler()) | ||||
| 
 | ||||
| PATH_PREFIX = "/matrix/client/api/v1" | ||||
| PATH_PREFIX = "/_matrix/client/api/v1" | ||||
| 
 | ||||
| 
 | ||||
| class EventStreamPaginationApiTestCase(unittest.TestCase): | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ ONLINE = PresenceState.ONLINE | |||
| 
 | ||||
| 
 | ||||
| myid = "@apple:test" | ||||
| PATH_PREFIX = "/matrix/client/api/v1" | ||||
| PATH_PREFIX = "/_matrix/client/api/v1" | ||||
| 
 | ||||
| 
 | ||||
| class JustPresenceHandlers(object): | ||||
|  | @ -229,7 +229,7 @@ class PresenceEventStreamTestCase(unittest.TestCase): | |||
|         # HIDEOUS HACKERY | ||||
|         # TODO(paul): This should be injected in via the HomeServer DI system | ||||
|         from synapse.streams.events import ( | ||||
|             PresenceSource, NullSource, EventSources | ||||
|             PresenceEventSource, NullSource, EventSources | ||||
|         ) | ||||
| 
 | ||||
|         old_SOURCE_TYPES = EventSources.SOURCE_TYPES | ||||
|  | @ -240,7 +240,7 @@ class PresenceEventStreamTestCase(unittest.TestCase): | |||
|         EventSources.SOURCE_TYPES = { | ||||
|             k: NullSource for k in old_SOURCE_TYPES.keys() | ||||
|         } | ||||
|         EventSources.SOURCE_TYPES["presence"] = PresenceSource | ||||
|         EventSources.SOURCE_TYPES["presence"] = PresenceEventSource | ||||
| 
 | ||||
|         hs = HomeServer("test", | ||||
|             db_pool=None, | ||||
|  | @ -274,6 +274,15 @@ class PresenceEventStreamTestCase(unittest.TestCase): | |||
|                 lambda u: defer.succeed([])) | ||||
| 
 | ||||
|         self.mock_datastore = hs.get_datastore() | ||||
| 
 | ||||
|         def get_profile_displayname(user_id): | ||||
|             return defer.succeed("Frank") | ||||
|         self.mock_datastore.get_profile_displayname = get_profile_displayname | ||||
| 
 | ||||
|         def get_profile_avatar_url(user_id): | ||||
|             return defer.succeed(None) | ||||
|         self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url | ||||
| 
 | ||||
|         self.presence = hs.get_handlers().presence_handler | ||||
| 
 | ||||
|         self.u_apple = hs.parse_userid("@apple:test") | ||||
|  | @ -295,7 +304,9 @@ class PresenceEventStreamTestCase(unittest.TestCase): | |||
|         # all be ours | ||||
| 
 | ||||
|         # I'll already get my own presence state change | ||||
|         self.assertEquals({"start": "0_1", "end": "0_1", "chunk": []}, response) | ||||
|         self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []}, | ||||
|             response | ||||
|         ) | ||||
| 
 | ||||
|         self.mock_datastore.set_presence_state.return_value = defer.succeed( | ||||
|                 {"state": ONLINE}) | ||||
|  | @ -306,14 +317,15 @@ class PresenceEventStreamTestCase(unittest.TestCase): | |||
|                 state={"state": ONLINE}) | ||||
| 
 | ||||
|         (code, response) = yield self.mock_resource.trigger("GET", | ||||
|                 "/events?from=0_1&timeout=0", None) | ||||
|                 "/events?from=0_1_0&timeout=0", None) | ||||
| 
 | ||||
|         self.assertEquals(200, code) | ||||
|         self.assertEquals({"start": "0_1", "end": "0_2", "chunk": [ | ||||
|         self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [ | ||||
|             {"type": "m.presence", | ||||
|              "content": { | ||||
|                  "user_id": "@banana:test", | ||||
|                  "state": ONLINE, | ||||
|                  "displayname": "Frank", | ||||
|                  "mtime_age": 0, | ||||
|             }}, | ||||
|         ]}, response) | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ from synapse.api.errors import SynapseError, AuthError | |||
| from synapse.server import HomeServer | ||||
| 
 | ||||
| myid = "@1234ABCD:test" | ||||
| PATH_PREFIX = "/matrix/client/api/v1" | ||||
| PATH_PREFIX = "/_matrix/client/api/v1" | ||||
| 
 | ||||
| class ProfileTestCase(unittest.TestCase): | ||||
|     """ Tests profile management. """ | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ from .utils import RestTestCase | |||
| 
 | ||||
| from mock import Mock | ||||
| 
 | ||||
| PATH_PREFIX = "/matrix/client/api/v1" | ||||
| PATH_PREFIX = "/_matrix/client/api/v1" | ||||
| 
 | ||||
| 
 | ||||
| class RoomPermissionsTestCase(RestTestCase): | ||||
|  |  | |||
|  | @ -37,6 +37,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even | |||
|         mPresence.start(); | ||||
|     } | ||||
|      | ||||
|     $scope.user_id = matrixService.config().user_id; | ||||
|      | ||||
|     /** | ||||
|      * Open a given page. | ||||
|      * @param {String} url url of the page | ||||
|  | @ -45,6 +47,16 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even | |||
|         $location.url(url); | ||||
|     }; | ||||
|      | ||||
|     // Open the given user profile page
 | ||||
|     $scope.goToUserPage = function(user_id) { | ||||
|         if (user_id === $scope.user_id) { | ||||
|             $location.url("/settings"); | ||||
|         } | ||||
|         else { | ||||
|             $location.url("/user/" + user_id); | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|     // Logs the user out 
 | ||||
|     $scope.logout = function() { | ||||
|          | ||||
|  | @ -69,11 +81,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even | |||
|         $scope.logout(); | ||||
|     }); | ||||
|      | ||||
|     $scope.requestNotifications = function() { | ||||
|         if (window.Notification) { | ||||
|             console.log("Notification.permission: " + window.Notification.permission); | ||||
|             window.Notification.requestPermission(function(){}); | ||||
|         } | ||||
|     $scope.updateHeader = function() { | ||||
|         $scope.user_id = matrixService.config().user_id; | ||||
|     }; | ||||
|      | ||||
| }]); | ||||
|  |  | |||
|  | @ -32,7 +32,12 @@ angular.module('matrixWebClient') | |||
| .directive('ngFocus', ['$timeout', function($timeout) { | ||||
|     return { | ||||
|         link: function(scope, element, attr) { | ||||
|             $timeout(function() { element[0].focus(); }, 0); | ||||
|             // XXX: slightly evil hack to disable autofocus on iOS, as in general
 | ||||
|             // it causes more problems than it fixes, by bouncing the page
 | ||||
|             // around
 | ||||
|             if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) { | ||||
|                 $timeout(function() { element[0].focus(); }, 0); | ||||
|             }             | ||||
|         } | ||||
|     }; | ||||
| }]); | ||||
							
								
								
									
										323
									
								
								webclient/app.css
									
										
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										323
									
								
								webclient/app.css
									
										
									
									
									
										
										
										Normal file → Executable file
									
								
							|  | @ -1,121 +1,194 @@ | |||
| /*** Mobile voodoo ***/ | ||||
| @media all and (max-device-width: 640px) { | ||||
|              | ||||
|     #messageTableWrapper { | ||||
|         margin-right: 0px ! important; | ||||
|     } | ||||
|      | ||||
|     .leftBlock { | ||||
|         width: 8em ! important; | ||||
|         font-size: 8px ! important; | ||||
|     } | ||||
|      | ||||
|     .rightBlock { | ||||
|         width: 0px ! important; | ||||
|         display: none ! important; | ||||
|     } | ||||
|      | ||||
|     .avatar { | ||||
|         width: 36px ! important; | ||||
|     } | ||||
|      | ||||
|     #header, | ||||
|     #messageTable, | ||||
|     #wrapper, | ||||
|     #roomName, | ||||
|     #controls { | ||||
|         max-width: 640px ! important; | ||||
|     }     | ||||
|      | ||||
|     #userIdCell, | ||||
|     #usersTableWrapper, | ||||
|     #extraControls { | ||||
|         display: none; | ||||
|     } | ||||
|      | ||||
|     #buttonsCell { | ||||
|         width: 60px ! important; | ||||
|         padding-left: 20px ! important; | ||||
|     } | ||||
|      | ||||
|     #roomLogo { | ||||
|         display: none; | ||||
|     } | ||||
|      | ||||
|     #roomName { | ||||
|         text-align: left ! important; | ||||
|         top: -35px ! important; | ||||
|     } | ||||
|      | ||||
|     .bubble { | ||||
|         font-size: 12px ! important; | ||||
|         min-height: 20px ! important; | ||||
|     } | ||||
|      | ||||
|     #page { | ||||
|         top: 35px ! important; | ||||
|         bottom: 70px ! important; | ||||
|     } | ||||
|      | ||||
|     #header, | ||||
|     #page { | ||||
|         margin: 5px ! important; | ||||
|     } | ||||
|      | ||||
|     #header { | ||||
|         padding: 5px ! important; | ||||
|     } | ||||
|          | ||||
|     /* stop zoom on select */ | ||||
|     select:focus, | ||||
|     textarea, | ||||
|     input | ||||
|     { | ||||
|         font-size: 16px ! important; | ||||
|     } | ||||
|      | ||||
| /** Common layout **/ | ||||
| 
 | ||||
| html { | ||||
|     height: 100%; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|     height: 100%; | ||||
|     font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif; | ||||
|     font-size: 12pt; | ||||
|     margin: 0px; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
|     font-family: Helvetica, Arial, sans-serif; | ||||
|     font-size: 20pt; | ||||
| } | ||||
| 
 | ||||
| /*** Overall page layout ***/ | ||||
| a:link    { color: #666; } | ||||
| a:visited { color: #666; } | ||||
| a:hover   { color: #000; } | ||||
| a:active  { color: #000; } | ||||
| 
 | ||||
| #page { | ||||
|     position: absolute; | ||||
|     top: 80px; | ||||
|     bottom: 100px; | ||||
|     left: 0px; | ||||
|     right: 0px; | ||||
|     margin: 20px; | ||||
|     min-height: 100%; | ||||
|     margin-bottom: -32px; /* to make room for the footer */ | ||||
| } | ||||
| 
 | ||||
| #wrapper { | ||||
|     margin: auto; | ||||
|     max-width: 1280px; | ||||
|     padding-top: 40px; | ||||
|     padding-bottom: 40px; | ||||
|     padding-left: 20px; | ||||
|     padding-right: 20px; | ||||
| } | ||||
| 
 | ||||
| #header | ||||
| { | ||||
|     position: absolute; | ||||
|     top: 0px; | ||||
|     width: 100%; | ||||
|     background-color: #333; | ||||
|     height: 32px; | ||||
| } | ||||
| 
 | ||||
| #headerContent { | ||||
|     color: #ccc; | ||||
|     max-width: 1280px; | ||||
|     margin: auto; | ||||
|     text-align: right; | ||||
|     height: 32px; | ||||
|     line-height: 32px; | ||||
| } | ||||
| 
 | ||||
| #headerContent a:link, | ||||
| #headerContent a:visited, | ||||
| #headerContent a:hover, | ||||
| #headerContent a:active { | ||||
|     color: #fff; | ||||
| } | ||||
| 
 | ||||
| #footer | ||||
| { | ||||
|     width: 100%; | ||||
|     border-top: #666 1px solid; | ||||
|     background-color: #aaa; | ||||
|     height: 32px; | ||||
| } | ||||
| 
 | ||||
| #footerContent | ||||
| { | ||||
|     font-size: 8pt; | ||||
|     color: #fff; | ||||
|     max-width: 1280px; | ||||
|     margin: auto; | ||||
|     text-align: center; | ||||
|     height: 32px; | ||||
|     line-height: 32px; | ||||
| } | ||||
| 
 | ||||
| #genericHeading | ||||
| { | ||||
|     margin-top: 13px; | ||||
| } | ||||
| 
 | ||||
| #feedback { | ||||
|     color: #800; | ||||
| } | ||||
| 
 | ||||
| .mouse-pointer { | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .invited { | ||||
|     opacity: 0.2; | ||||
| } | ||||
| 
 | ||||
| /*** Login Pages ***/ | ||||
| 
 | ||||
| .loginWrapper { | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| #loginForm { | ||||
|     text-align: left; | ||||
|     padding: 1em; | ||||
|     margin-bottom: 40px; | ||||
|     display: inline-block; | ||||
|      | ||||
|     -webkit-border-radius: 10px; | ||||
|     -moz-border-radius: 10px; | ||||
|     border-radius: 10px; | ||||
|      | ||||
|     -webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); | ||||
|     -moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); | ||||
|     box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); | ||||
|      | ||||
|     background-color: #f8f8f8; | ||||
|     border: 1px #ccc solid; | ||||
| } | ||||
| 
 | ||||
| #loginForm input[type='radio'] { | ||||
|     margin-right: 1em; | ||||
| } | ||||
| 
 | ||||
| #serverConfig { | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| #serverConfig, | ||||
| #serverConfig input, | ||||
| #serverConfig button | ||||
| { | ||||
|     font-size: 10pt ! important; | ||||
| } | ||||
| 
 | ||||
| .smallPrint { | ||||
|     color: #888; | ||||
|     font-size: 9pt ! important; | ||||
|     font-style: italic ! important; | ||||
| } | ||||
| 
 | ||||
| #serverConfig label { | ||||
|     display: inline-block; | ||||
|     text-align: right; | ||||
|     margin-right: 0.5em; | ||||
|     width: 7em;    | ||||
| } | ||||
| 
 | ||||
| #loginForm, | ||||
| #loginForm input, | ||||
| #loginForm button, | ||||
| #loginForm select { | ||||
|     font-size: 18px; | ||||
| } | ||||
| 
 | ||||
| /*** Room page ***/ | ||||
| 
 | ||||
| #roomPage { | ||||
|     position: absolute; | ||||
|     top: 120px; | ||||
|     bottom: 120px; | ||||
|     left: 20px; | ||||
|     right: 20px; | ||||
| } | ||||
| 
 | ||||
| #roomWrapper { | ||||
|     margin: auto; | ||||
|     max-width: 1280px; | ||||
|     height: 100%; | ||||
| } | ||||
| 
 | ||||
| #roomName { | ||||
|     max-width: 1280px; | ||||
|     width: 100%; | ||||
|     text-align: right; | ||||
|     top: -40px; | ||||
|     position: absolute; | ||||
|     float: right; | ||||
|     font-size: 16px; | ||||
|     margin-top: 15px; | ||||
| } | ||||
| 
 | ||||
| #roomHeader { | ||||
|     margin: auto; | ||||
|     padding-left: 20px; | ||||
|     padding-right: 20px; | ||||
|     padding-top: 53px; | ||||
|     max-width: 1280px; | ||||
| } | ||||
| 
 | ||||
| #controlPanel { | ||||
|     position: absolute; | ||||
|     bottom: 0px; | ||||
|     width: 100%; | ||||
|     height: 100px; | ||||
|     background-color: #f8f8f8; | ||||
|     border-top: #aaa 1px solid; | ||||
| } | ||||
|  | @ -146,10 +219,6 @@ h1 { | |||
|     background-color: #faa; | ||||
| } | ||||
| 
 | ||||
| .mouse-pointer { | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| /*** Participant list ***/ | ||||
| 
 | ||||
| #usersTableWrapper { | ||||
|  | @ -300,7 +369,7 @@ h1 { | |||
|     display: inline-block; | ||||
|     margin-bottom: -1px; | ||||
|     max-width: 90%; | ||||
|     font-size: 16px; | ||||
|     font-size: 14px; | ||||
|     word-wrap: break-word; | ||||
|     padding-top: 7px; | ||||
|     padding-bottom: 5px; | ||||
|  | @ -310,6 +379,11 @@ h1 { | |||
|     -webkit-text-size-adjust:100% | ||||
| } | ||||
| 
 | ||||
| .bubble img { | ||||
|     max-width: 100%; | ||||
|     max-height: auto; | ||||
| } | ||||
| 
 | ||||
| .differentUser td { | ||||
|     padding-bottom: 5px ! important; | ||||
| } | ||||
|  | @ -341,8 +415,8 @@ h1 { | |||
| } | ||||
| 
 | ||||
| #room-fullscreen-image img { | ||||
|     max-width: 100%; | ||||
|     max-height: 100%; | ||||
|     max-width: 90%; | ||||
|     max-height: 90%; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     margin: auto; | ||||
|  | @ -350,9 +424,14 @@ h1 { | |||
|     position: fixed; | ||||
|     right: 0; | ||||
|     top: 0; | ||||
| 
 | ||||
|     -webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75); | ||||
|     -moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75); | ||||
|     box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);     | ||||
| } | ||||
| 
 | ||||
| /*** Recents ***/ | ||||
| 
 | ||||
| .recentsTable { | ||||
|     max-width: 480px; | ||||
|     width: 100%; | ||||
|  | @ -402,11 +481,14 @@ h1 { | |||
| } | ||||
| 
 | ||||
| /*** Recents in the room page ***/ | ||||
| 
 | ||||
| #roomRecentsTableWrapper { | ||||
|     float: left; | ||||
|     max-width: 320px; | ||||
|     margin-right: 20px; | ||||
|     padding-right: 10px; | ||||
|     margin-right: 10px; | ||||
|     height: 100%; | ||||
|     border-right: 1px solid #ddd; | ||||
|     overflow-y: auto; | ||||
| } | ||||
| 
 | ||||
|  | @ -421,55 +503,14 @@ h1 { | |||
| } | ||||
| 
 | ||||
| .profile-avatar img { | ||||
|     max-width: 100%; | ||||
|     max-height: 100%; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     object-fit: cover;     | ||||
| } | ||||
| 
 | ||||
| /*** User profile page ***/ | ||||
| #user-ids { | ||||
|     padding-left: 1em; | ||||
| } | ||||
| 
 | ||||
| #user-displayname { | ||||
|     font-size: 24px; | ||||
| } | ||||
| /******************************/ | ||||
| 
 | ||||
| #header | ||||
| { | ||||
|     padding: 20px; | ||||
|     max-width: 1280px; | ||||
|     margin: auto; | ||||
| } | ||||
| 
 | ||||
| #logo, | ||||
| #roomLogo { | ||||
|     max-width: 1280px; | ||||
|     margin: auto; | ||||
| } | ||||
| 
 | ||||
| #header-buttons { | ||||
|     float: right; | ||||
| } | ||||
| 
 | ||||
| .text_entry_section { | ||||
|     position: fixed; | ||||
|     bottom: 0; | ||||
|     z-index: 100; | ||||
|     left: 0; | ||||
|     right: 10em; | ||||
|     width: 100%; | ||||
|     background: #e0e0e0; | ||||
| } | ||||
| 
 | ||||
| .member_invited { | ||||
|     color: blue; | ||||
| } | ||||
| 
 | ||||
| .member_joined { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| .member_left { | ||||
|     color: gray; | ||||
| } | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ var matrixWebClient = angular.module('matrixWebClient', [ | |||
|     'ngRoute', | ||||
|     'MatrixWebClientController', | ||||
|     'LoginController', | ||||
|     'RegisterController', | ||||
|     'RoomController', | ||||
|     'HomeController', | ||||
|     'RecentsController', | ||||
|  | @ -38,6 +39,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', | |||
|                 templateUrl: 'login/login.html', | ||||
|                 controller: 'LoginController' | ||||
|             }). | ||||
|             when('/register', { | ||||
|                 templateUrl: 'login/register.html', | ||||
|                 controller: 'RegisterController' | ||||
|             }). | ||||
|             when('/room/:room_id_or_alias', { | ||||
|                 templateUrl: 'room/room.html', | ||||
|                 controller: 'RoomController' | ||||
|  | @ -84,7 +89,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', | |||
| matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) { | ||||
| 
 | ||||
|     // If user auth details are not in cache, go to the login page
 | ||||
|     if (!matrixService.isUserLoggedIn()) { | ||||
|     if (!matrixService.isUserLoggedIn() && | ||||
|         $location.path() !== "/login" && | ||||
|         $location.path() !== "/register") | ||||
|     { | ||||
|         $location.path("login"); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities']) | |||
|      */ | ||||
|     this.uploadFile = function(file) { | ||||
|         var deferred = $q.defer(); | ||||
|         console.log("Uploading " + file.name + "... to /matrix/content"); | ||||
|         console.log("Uploading " + file.name + "... to /_matrix/content"); | ||||
|         matrixService.uploadContent(file).then( | ||||
|             function(response) { | ||||
|                 var content_url = response.data.content_token; | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ var forAllTracksOnStream = function(s, f) { | |||
| } | ||||
| 
 | ||||
| angular.module('MatrixCall', []) | ||||
| .factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) { | ||||
| .factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope) { | ||||
|     var MatrixCall = function(room_id) { | ||||
|         this.room_id = room_id; | ||||
|         this.call_id = "c" + new Date().getTime(); | ||||
|  | @ -73,9 +73,7 @@ angular.module('MatrixCall', []) | |||
|         this.state = 'wait_local_media'; | ||||
|     }; | ||||
| 
 | ||||
|     MatrixCall.prototype.hangup = function() { | ||||
|         console.trace("Ending call "+this.call_id); | ||||
| 
 | ||||
|     MatrixCall.prototype.stopAllMedia = function() { | ||||
|         if (this.localAVStream) { | ||||
|             forAllTracksOnStream(this.localAVStream, function(t) { | ||||
|                 t.stop(); | ||||
|  | @ -86,6 +84,12 @@ angular.module('MatrixCall', []) | |||
|                 t.stop(); | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     MatrixCall.prototype.hangup = function() { | ||||
|         console.trace("Ending call "+this.call_id); | ||||
| 
 | ||||
|         this.stopAllMedia(); | ||||
| 
 | ||||
|         var content = { | ||||
|             version: 0, | ||||
|  | @ -204,6 +208,7 @@ angular.module('MatrixCall', []) | |||
|         // ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
 | ||||
|         if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') { | ||||
|             this.state = 'connected'; | ||||
|             $rootScope.$apply(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|  | @ -232,8 +237,9 @@ angular.module('MatrixCall', []) | |||
|             t.onstarted = self.onRemoteStreamTrackStarted; | ||||
|         }); | ||||
| 
 | ||||
|         event.stream.onended = function(e) { self.onRemoteStreamEnded(e); };  | ||||
|         // not currently implemented in chrome
 | ||||
|         event.stream.onstarted = this.onRemoteStreamStarted; | ||||
|         event.stream.onstarted = function(e) { self.onRemoteStreamStarted(e); }; | ||||
|         var player = new Audio(); | ||||
|         player.src = URL.createObjectURL(s); | ||||
|         player.play(); | ||||
|  | @ -243,24 +249,19 @@ angular.module('MatrixCall', []) | |||
|         this.state = 'connected'; | ||||
|     }; | ||||
| 
 | ||||
|     MatrixCall.prototype.onRemoteStreamEnded = function(event) { | ||||
|         this.state = 'ended'; | ||||
|         this.stopAllMedia(); | ||||
|         this.onHangup(); | ||||
|     }; | ||||
| 
 | ||||
|     MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) { | ||||
|         this.state = 'connected'; | ||||
|     }; | ||||
| 
 | ||||
|     MatrixCall.prototype.onHangupReceived = function() { | ||||
|         this.state = 'ended'; | ||||
| 
 | ||||
|         if (this.localAVStream) { | ||||
|             forAllTracksOnStream(this.localAVStream, function(t) { | ||||
|                 t.stop(); | ||||
|             }); | ||||
|         } | ||||
|         if (this.remoteAVStream) { | ||||
|             forAllTracksOnStream(this.remoteAVStream, function(t) { | ||||
|                 t.stop(); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         this.stopAllMedia(); | ||||
|         this.onHangup(); | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -38,7 +38,7 @@ angular.module('matrixService', []) | |||
|      | ||||
|     // Current version of permanent storage
 | ||||
|     var configVersion = 0; | ||||
|     var prefixPath = "/matrix/client/api/v1"; | ||||
|     var prefixPath = "/_matrix/client/api/v1"; | ||||
|     var MAPPING_PREFIX = "alias_for_"; | ||||
| 
 | ||||
|     var doRequest = function(method, path, params, data, $httpParams) { | ||||
|  | @ -95,14 +95,18 @@ angular.module('matrixService', []) | |||
|         }, | ||||
| 
 | ||||
|         // Create a room
 | ||||
|         create: function(room_id, visibility) { | ||||
|         create: function(room_alias, visibility) { | ||||
|             // The REST path spec
 | ||||
|             var path = "/createRoom"; | ||||
| 
 | ||||
|             return doRequest("POST", path, undefined, { | ||||
|                 visibility: visibility, | ||||
|                 room_alias_name: room_id | ||||
|             }); | ||||
|             var req = { | ||||
|                 "visibility": visibility | ||||
|             }; | ||||
|             if (room_alias) { | ||||
|                 req.room_alias_name = room_alias; | ||||
|             } | ||||
|              | ||||
|             return doRequest("POST", path, undefined, req); | ||||
|         }, | ||||
| 
 | ||||
|         // List all rooms joined or been invited to
 | ||||
|  | @ -164,7 +168,7 @@ angular.module('matrixService', []) | |||
| 
 | ||||
|         // Retrieves the room ID corresponding to a room alias
 | ||||
|         resolveRoomAlias:function(room_alias) { | ||||
|             var path = "/matrix/client/api/v1/directory/room/$room_alias"; | ||||
|             var path = "/_matrix/client/api/v1/directory/room/$room_alias"; | ||||
|             room_alias = encodeURIComponent(room_alias); | ||||
| 
 | ||||
|             path = path.replace("$room_alias", room_alias); | ||||
|  | @ -304,7 +308,7 @@ angular.module('matrixService', []) | |||
| 
 | ||||
|         // hit the Identity Server for a 3PID request.
 | ||||
|         linkEmail: function(email, clientSecret, sendAttempt) { | ||||
|             var path = "/matrix/identity/api/v1/validate/email/requestToken" | ||||
|             var path = "/_matrix/identity/api/v1/validate/email/requestToken" | ||||
|             var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt; | ||||
|             var headers = {}; | ||||
|             headers["Content-Type"] = "application/x-www-form-urlencoded"; | ||||
|  | @ -312,7 +316,7 @@ angular.module('matrixService', []) | |||
|         }, | ||||
| 
 | ||||
|         authEmail: function(clientSecret, tokenId, code) { | ||||
|             var path = "/matrix/identity/api/v1/validate/email/submitToken"; | ||||
|             var path = "/_matrix/identity/api/v1/validate/email/submitToken"; | ||||
|             var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret; | ||||
|             var headers = {}; | ||||
|             headers["Content-Type"] = "application/x-www-form-urlencoded"; | ||||
|  | @ -320,7 +324,7 @@ angular.module('matrixService', []) | |||
|         }, | ||||
| 
 | ||||
|         bindEmail: function(userId, tokenId, clientSecret) { | ||||
|             var path = "/matrix/identity/api/v1/3pid/bind"; | ||||
|             var path = "/_matrix/identity/api/v1/3pid/bind"; | ||||
|             var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret; | ||||
|             var headers = {}; | ||||
|             headers["Content-Type"] = "application/x-www-form-urlencoded"; | ||||
|  | @ -328,7 +332,7 @@ angular.module('matrixService', []) | |||
|         }, | ||||
|          | ||||
|         uploadContent: function(file) { | ||||
|             var path = "/matrix/content"; | ||||
|             var path = "/_matrix/content"; | ||||
|             var headers = { | ||||
|                 "Content-Type": undefined // undefined means angular will figure it out
 | ||||
|             }; | ||||
|  |  | |||
|  | @ -23,9 +23,9 @@ | |||
| angular.module('mPresence', []) | ||||
| .service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) { | ||||
| 
 | ||||
|     // Time in ms after that a user is considered as offline/away
 | ||||
|     var OFFLINE_TIME = 5 * 60000; // 5 mins
 | ||||
|      | ||||
|     // Time in ms after that a user is considered as unavailable/away
 | ||||
|     var UNAVAILABLE_TIME = 5 * 60000; // 5 mins
 | ||||
|     | ||||
|     // The current presence state
 | ||||
|     var state = undefined; | ||||
| 
 | ||||
|  | @ -88,11 +88,11 @@ angular.module('mPresence', []) | |||
|     }; | ||||
|      | ||||
|     /** | ||||
|      * Callback called when the user made no action on the page for OFFLINE_TIME ms. | ||||
|      * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. | ||||
|      * @private | ||||
|      */ | ||||
|     function onOfflineTimerFire() { | ||||
|         self.setState(matrixService.presence.offline); | ||||
|     function onUnvailableTimerFire() { | ||||
|         self.setState(matrixService.presence.unavailable); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -105,7 +105,7 @@ angular.module('mPresence', []) | |||
|          | ||||
|         // Re-arm the timer
 | ||||
|         $timeout.cancel(timer); | ||||
|         timer = $timeout(onOfflineTimerFire, OFFLINE_TIME); | ||||
|         timer = $timeout(onUnvailableTimerFire, UNAVAILABLE_TIME); | ||||
|     }     | ||||
| 
 | ||||
| }]); | ||||
|  |  | |||
|  | @ -37,6 +37,11 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen | |||
|     $scope.joinAlias = { | ||||
|         room_alias: "" | ||||
|     }; | ||||
|      | ||||
|     $scope.profile = { | ||||
|         displayName: "", | ||||
|         avatarUrl: "" | ||||
|     }; | ||||
| 
 | ||||
|     var refresh = function() { | ||||
|          | ||||
|  | @ -53,14 +58,14 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen | |||
|         ); | ||||
|     }; | ||||
|      | ||||
|     $scope.createNewRoom = function(room_id, isPrivate) { | ||||
|     $scope.createNewRoom = function(room_alias, isPrivate) { | ||||
|          | ||||
|         var visibility = "public"; | ||||
|         if (isPrivate) { | ||||
|             visibility = "private"; | ||||
|         } | ||||
|          | ||||
|         matrixService.create(room_id, visibility).then( | ||||
|         matrixService.create(room_alias, visibility).then( | ||||
|             function(response) {  | ||||
|                 // This room has been created. Refresh the rooms list
 | ||||
|                 console.log("Created room " + response.data.room_alias + " with id: "+ | ||||
|  | @ -108,6 +113,26 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen | |||
|     }; | ||||
|   | ||||
|     $scope.onInit = function() { | ||||
|         // Load profile data
 | ||||
|         // Display name
 | ||||
|         matrixService.getDisplayName($scope.config.user_id).then( | ||||
|             function(response) { | ||||
|                 $scope.profile.displayName = response.data.displayname; | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Can't load display name"; | ||||
|             }  | ||||
|         ); | ||||
|         // Avatar
 | ||||
|         matrixService.getProfilePictureUrl($scope.config.user_id).then( | ||||
|             function(response) { | ||||
|                 $scope.profile.avatarUrl = response.data.avatar_url; | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Can't load avatar URL"; | ||||
|             }  | ||||
|         ); | ||||
| 
 | ||||
|         refresh(); | ||||
|     }; | ||||
| }]); | ||||
|  |  | |||
|  | @ -1,29 +1,24 @@ | |||
| <div ng-controller="HomeController" data-ng-init="onInit()"> | ||||
| 
 | ||||
|     <div id="page"> | ||||
|     <div id="wrapper"> | ||||
|          | ||||
|     <div> | ||||
|         <form> | ||||
|             <table> | ||||
|                 <tr> | ||||
|                     <td> | ||||
|                         <div class="profile-avatar"> | ||||
|                             <img ng-src="{{ config.avatarUrl || 'img/default-profile.jpg' }}"/> | ||||
|                         </div> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         <div id="user-ids"> | ||||
|                             <div id="user-displayname">{{ config.displayName }}</div> | ||||
|                             <div>{{ config.user_id }}</div>                         | ||||
|                         </div> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             </table> | ||||
|         </form> | ||||
|      | ||||
|     <div id="genericHeading"> | ||||
|         <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> | ||||
|     </div> | ||||
|      | ||||
|     <h3>Recents</h3> | ||||
|     <h1>Welcome to homeserver {{ config.homeserver }}</h1> | ||||
|      | ||||
|     <div> | ||||
|         <div class="profile-avatar"> | ||||
|             <img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}"/> | ||||
|         </div> | ||||
|         <div id="user-ids"> | ||||
|             <div id="user-displayname">{{ profile.displayName }}</div> | ||||
|             <div>{{ config.user_id }}</div> | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <h3>Recent conversations</h3> | ||||
|         <div ng-include="'recents/recents.html'"></div> | ||||
|     <br/> | ||||
| 
 | ||||
|  | @ -38,9 +33,9 @@ | |||
|      | ||||
|     <div> | ||||
|         <form> | ||||
|             <input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/> | ||||
|             <input size="40" ng-model="newRoom.room_alias" ng-enter="createNewRoom(newRoom.room_alias, newRoom.private)" placeholder="(e.g. foo_channel)"/> | ||||
|             <input type="checkbox" ng-model="newRoom.private">private | ||||
|             <button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>     | ||||
|             <button ng-disabled="!newRoom.room_alias" ng-click="createNewRoom(newRoom.room_alias, newRoom.private)">Create room</button>     | ||||
|         </form> | ||||
|     </div> | ||||
|     <div> | ||||
|  | @ -54,5 +49,4 @@ | |||
|     {{ feedback }} | ||||
| 
 | ||||
|     </div>     | ||||
|     </div> | ||||
| </div> | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 2.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								webclient/img/default-profile.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								webclient/img/default-profile.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								webclient/img/logo-small.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								webclient/img/logo-small.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 910 B | 
							
								
								
									
										
											BIN
										
									
								
								webclient/img/logo.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								webclient/img/logo.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4 KiB | 
|  | @ -4,6 +4,8 @@ | |||
|     <title>[matrix]</title> | ||||
|          | ||||
|     <link rel="stylesheet" href="app.css"> | ||||
|     <link rel="stylesheet" href="mobile.css"> | ||||
|      | ||||
|     <link rel="icon" href="favicon.ico"> | ||||
|     | ||||
|     <meta name="viewport" content="width=device-width"> | ||||
|  | @ -19,6 +21,7 @@ | |||
|     <script src="app-filter.js"></script> | ||||
|     <script src="home/home-controller.js"></script> | ||||
|     <script src="login/login-controller.js"></script> | ||||
|     <script src="login/register-controller.js"></script> | ||||
|     <script src="recents/recents-controller.js"></script> | ||||
|     <script src="recents/recents-filter.js"></script> | ||||
|     <script src="room/room-controller.js"></script> | ||||
|  | @ -38,15 +41,23 @@ | |||
| 
 | ||||
| <body> | ||||
| 
 | ||||
|     <header id="header"> | ||||
|     <div id="header"> | ||||
|         <!-- Do not show buttons on the login page --> | ||||
|         <div id="header-buttons" ng-hide="'/login' == location "> | ||||
|         <div id="headerContent" ng-hide="'/login' == location || '/register' == location"> | ||||
|             <a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a> | ||||
|               | ||||
|             <button ng-click='goToPage("/")'>Home</button> | ||||
|             <button ng-click='goToPage("settings")'>Settings</button> | ||||
|             <button ng-click="logout()">Log out</button> | ||||
|         </div> | ||||
|     </header> | ||||
|     </div> | ||||
| 
 | ||||
|     <div ng-view></div> | ||||
|     <div id="page" ng-view></div> | ||||
| 
 | ||||
|     <div id="footer" ng-hide="location.indexOf('/room') == 0"> | ||||
|         <div id="footerContent"> | ||||
|             © 2014 Matrix.org | ||||
|         </div> | ||||
|     </div> | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -1,3 +1,19 @@ | |||
| /* | ||||
|  Copyright 2014 matrix.org | ||||
|   | ||||
|  Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|   | ||||
|  http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|   | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  See the License for the specific language governing permissions and | ||||
|  limitations under the License. | ||||
|  */ | ||||
|   | ||||
| angular.module('LoginController', ['matrixService']) | ||||
| .controller('LoginController', ['$scope', '$location', 'matrixService', 'eventStreamService', | ||||
|                                     function($scope, $location, matrixService, eventStreamService) { | ||||
|  | @ -7,7 +23,10 @@ angular.module('LoginController', ['matrixService']) | |||
|     // Assume that this is hosted on the home server, in which case the URL
 | ||||
|     // contains the home server.
 | ||||
|     var hs_url = $location.protocol() + "://" + $location.host(); | ||||
|     if ($location.port()) { | ||||
|     if ($location.port() && | ||||
|         !($location.protocol() === "http" && $location.port() === 80) && | ||||
|         !($location.protocol() === "https" && $location.port() === 443)) | ||||
|     { | ||||
|         hs_url += ":" + $location.port(); | ||||
|     } | ||||
|      | ||||
|  | @ -16,57 +35,18 @@ angular.module('LoginController', ['matrixService']) | |||
|         desired_user_name: "", | ||||
|         user_id: "", | ||||
|         password: "", | ||||
|         identityServer: "", | ||||
|         identityServer: "http://matrix.org:8090", | ||||
|         pwd1: "", | ||||
|         pwd2: "" | ||||
|         pwd2: "", | ||||
|     }; | ||||
| 
 | ||||
|     $scope.register = function() { | ||||
| 
 | ||||
|         // Set the urls
 | ||||
|         matrixService.setConfig({ | ||||
|             homeserver: $scope.account.homeserver, | ||||
|             identityServer: $scope.account.identityServer | ||||
|         }); | ||||
|          | ||||
|         if ($scope.account.pwd1 !== $scope.account.pwd2) { | ||||
|             $scope.feedback = "Passwords don't match."; | ||||
|             return; | ||||
|         } | ||||
|         else if ($scope.account.pwd1.length < 6) { | ||||
|             $scope.feedback = "Password must be at least 6 characters."; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         matrixService.register($scope.account.desired_user_name, $scope.account.pwd1).then( | ||||
|             function(response) { | ||||
|                 $scope.feedback = "Success"; | ||||
|                 // Update the current config 
 | ||||
|                 var config = matrixService.config(); | ||||
|                 angular.extend(config, { | ||||
|                     access_token: response.data.access_token, | ||||
|                     user_id: response.data.user_id | ||||
|                 }); | ||||
|                 matrixService.setConfig(config); | ||||
| 
 | ||||
|                 // And permanently save it
 | ||||
|                 matrixService.saveConfig(); | ||||
|                 eventStreamService.resume(); | ||||
|                  // Go to the user's rooms list page
 | ||||
|                 $location.url("home"); | ||||
|             }, | ||||
|             function(error) { | ||||
|                 if (error.data) { | ||||
|                     if (error.data.errcode === "M_USER_IN_USE") { | ||||
|                         $scope.feedback = "Username already taken."; | ||||
|                     } | ||||
|                 } | ||||
|                 else if (error.status === 0) { | ||||
|                     $scope.feedback = "Unable to talk to the server."; | ||||
|                 } | ||||
|             }); | ||||
|      | ||||
|     $scope.login_types = [ "email", "mxid" ]; | ||||
|     $scope.login_type_label = { | ||||
|         "email": "Email address", | ||||
|         "mxid": "Matrix ID (e.g. @bob:matrix.org or bob)", | ||||
|     }; | ||||
| 
 | ||||
|     $scope.login_type = 'mxid'; // TODO: remember the user's preferred login_type
 | ||||
|      | ||||
|     $scope.login = function() { | ||||
|         matrixService.setConfig({ | ||||
|             homeserver: $scope.account.homeserver, | ||||
|  |  | |||
|  | @ -1,55 +1,49 @@ | |||
| <div ng-controller="LoginController" class="login">     | ||||
|     <h1 id="logo">[matrix]</h1> | ||||
| 
 | ||||
|     <div id="page"> | ||||
|     <div id="wrapper"> | ||||
| 
 | ||||
|     {{ feedback }} | ||||
|     <div id="wrapper" class="loginWrapper"> | ||||
|          | ||||
|     <h3>Register for an account:</h3> | ||||
|     <form novalidate> | ||||
|         <input id="desired_user_name" size="70" type="text" auto-focus ng-model="account.desired_user_name" placeholder="User name (ex:bob)"/> | ||||
|         <br/> | ||||
|         <input id="pwd1" size="70" type="password" auto-focus ng-model="account.pwd1" placeholder="Type a password"/> | ||||
|         <br/> | ||||
|         <input id="pwd2" size="70" type="password" auto-focus ng-model="account.pwd2" placeholder="Re-type your password"/> | ||||
|         <br/> | ||||
|         <!-- New user registration --> | ||||
|         <div> | ||||
|             <br/> | ||||
|             <button ng-click="register()" ng-disabled="!account.desired_user_name || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Register</button> | ||||
|         </div> | ||||
|     </form> | ||||
| 
 | ||||
|     <h3>Got an account?</h3> | ||||
|     <form novalidate> | ||||
|         <!-- Login with an registered user --> | ||||
|         <div>{{ login_error_msg }} </div> | ||||
|         <div> | ||||
|             <input id="user_id" size="70" type="text" auto-focus ng-model="account.user_id" placeholder="User ID (ex:@bob:localhost or bob)"/> | ||||
|             <br /> | ||||
|             <input id="password" size="70" type="password" ng-model="account.password" placeholder="Password"/><br /> | ||||
|             <br/> | ||||
|             <button ng-click="login()" ng-disabled="!account.user_id || !account.password || !account.homeserver">Login</button> | ||||
|         </div> | ||||
|         | ||||
|     </form> | ||||
| 
 | ||||
|     <h3>Servers</h3> | ||||
|     <form novalidate> | ||||
|         <div> | ||||
|         Home Server:  | ||||
|         <input id="homeserver" size="57" type="text" ng-model="account.homeserver" placeholder="Home server URL (ex: http://localhost:8080)"/> | ||||
|         </div> | ||||
|         <br /> | ||||
|         <div> | ||||
|         Identity Server:  | ||||
|         <input id="identityServer" size="56" type="text" ng-model="account.identityServer" placeholder="Identity server URL (ex: http://localhost:8090)"/> | ||||
|         </div> | ||||
|         <br /> | ||||
|     </form> | ||||
|     <br/> | ||||
|         <a href ng-click="goToPage('/')"> | ||||
|         <img src="img/logo.png" width="240" height="102" alt="[matrix]" style="padding: 50px"/> | ||||
|         </a> | ||||
|      | ||||
|         <br/> | ||||
| 
 | ||||
|         <form id="loginForm" novalidate> | ||||
|             <!-- Login with an registered user --> | ||||
|             <div> | ||||
|                 Log in using:<br/> | ||||
|                  | ||||
|                 <div ng-repeat="type in login_types"> | ||||
|                 <input type="radio" ng-model="$parent.login_type" value="{{ type }}" id="radio_{{ type }}"/> | ||||
|                 <label for="radio_{{ type }}">{{ login_type_label[type] }}</label> | ||||
|                 </div> | ||||
|                      | ||||
|                 <div style="text-align: center"> | ||||
|                     <br/> | ||||
|                     <input id="user_id" size="32" type="text" ng-focus="true" ng-model="account.user_id" placeholder="{{ login_type_label[login_type] }}"/> | ||||
|                     <br/> | ||||
|                     <input id="password" size="32" type="password" ng-model="account.password" placeholder="Password"/> | ||||
|                     <br/><br/> | ||||
|                     <button ng-click="login()" ng-disabled="!account.user_id || !account.password || !account.homeserver">Login</button> | ||||
|                     <br/><br/> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <div class="feedback">{{ feedback }} {{ login_error_msg }}</div> | ||||
|                  | ||||
|                 <div id="serverConfig"> | ||||
|                     <label for="homeserver">Home Server:</label>  | ||||
|                     <input id="homeserver" size="32" type="text" ng-model="account.homeserver" placeholder="URL (e.g. http://matrix.org:8080)"/> | ||||
|                     <div class="smallPrint">Your home server stores all your conversation and account data.</div> | ||||
|                     <label for="identityServer">Identity Server:</label> | ||||
|                     <input id="identityServer" size="32" type="text" ng-model="account.identityServer" placeholder="URL (e.g. http://matrix.org:8090)"/> | ||||
|                     <div class="smallPrint">Matrix provides identity servers to track which emails etc. belong to which Matrix IDs.<br/> | ||||
|                         Only http://matrix.org:8090 currently exists.</div> | ||||
|                     <br/> | ||||
|                     <br/> | ||||
|                     <a href="#/register" style="padding-right: 3em">Create account</a> | ||||
|                     <a href="#/reset_password">Forgotten password?</a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </form> | ||||
| 
 | ||||
|     </div> | ||||
|     </div> | ||||
|  |  | |||
							
								
								
									
										102
									
								
								webclient/login/register-controller.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								webclient/login/register-controller.js
									
										
									
									
									
										Normal 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."; | ||||
|                 } | ||||
|             }); | ||||
|     }; | ||||
| 
 | ||||
| }]); | ||||
| 
 | ||||
							
								
								
									
										48
									
								
								webclient/login/register.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								webclient/login/register.html
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										92
									
								
								webclient/mobile.css
									
										
									
									
									
										Normal 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; | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -88,7 +88,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|         call.onHangup = $scope.onCallHangup; | ||||
|         $scope.currentCall = call; | ||||
|     }); | ||||
| 
 | ||||
|      | ||||
|     $scope.memberCount = function() { | ||||
|         return Object.keys($scope.members).length; | ||||
|     }; | ||||
|  | @ -175,6 +175,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
| 
 | ||||
|         // set target_user_id to keep things clear
 | ||||
|         var target_user_id = chunk.state_key; | ||||
|          | ||||
|         var now = new Date().getTime(); | ||||
| 
 | ||||
|         var isNewMember = !(target_user_id in $scope.members); | ||||
|         if (isNewMember) { | ||||
|  | @ -185,44 +187,14 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|             if ("mtime_age" in chunk.content) { | ||||
|                 chunk.mtime_age = chunk.content.mtime_age; | ||||
|             } | ||||
|             // Once the HS reliably returns the displaynames & avatar_urls for both
 | ||||
|             // local and remote users, we should use this rather than the evalAsync block
 | ||||
|             // below
 | ||||
|             if ("displayname" in chunk.content) { | ||||
|                 chunk.displayname = chunk.content.displayname; | ||||
|             } | ||||
|             if ("avatar_url" in chunk.content) { | ||||
|                 chunk.avatar_url = chunk.content.avatar_url; | ||||
|             } | ||||
|             $scope.members[target_user_id] = chunk; | ||||
| 
 | ||||
| /* | ||||
|             // Stale code for explicitly hammering the homeserver for every displayname & avatar_url
 | ||||
|              | ||||
|             // get their display name and profile picture and set it to their
 | ||||
|             // member entry in $scope.members. We HAVE to use $timeout with 0 delay 
 | ||||
|             // to make this function run AFTER the current digest cycle, else the 
 | ||||
|             // response may update a STALE VERSION of the member list (manifesting
 | ||||
|             // as no member names appearing, or appearing sporadically).
 | ||||
|             $scope.$evalAsync(function() { | ||||
|                 matrixService.getDisplayName(chunk.target_user_id).then( | ||||
|                     function(response) { | ||||
|                         var member = $scope.members[chunk.target_user_id]; | ||||
|                         if (member !== undefined) { | ||||
|                             member.displayname = response.data.displayname; | ||||
|                         } | ||||
|                     } | ||||
|                 );  | ||||
|                 matrixService.getProfilePictureUrl(chunk.target_user_id).then( | ||||
|                     function(response) { | ||||
|                          var member = $scope.members[chunk.target_user_id]; | ||||
|                          if (member !== undefined) { | ||||
|                             member.avatar_url = response.data.avatar_url; | ||||
|                          } | ||||
|                     } | ||||
|                 ); | ||||
|             }); | ||||
| */             | ||||
|             chunk.last_updated = now; | ||||
|             $scope.members[target_user_id] = chunk;    | ||||
| 
 | ||||
|             if (target_user_id in $rootScope.presence) { | ||||
|                 updatePresence($rootScope.presence[target_user_id]); | ||||
|  | @ -234,6 +206,12 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|             member.content.membership = chunk.content.membership; | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|     var updateMemberListPresenceAge = function() { | ||||
|         $scope.now = new Date().getTime(); | ||||
|         // TODO: don't bother polling every 5s if we know none of our counters are younger than 1 minute
 | ||||
|         $timeout(updateMemberListPresenceAge, 5 * 1000); | ||||
|     }; | ||||
| 
 | ||||
|     var updatePresence = function(chunk) { | ||||
|         if (!(chunk.content.user_id in $scope.members)) { | ||||
|  | @ -275,6 +253,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|         if ($scope.textInput.indexOf("/me") === 0) { | ||||
|             promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4)); | ||||
|         } | ||||
|         else if ($scope.textInput.indexOf("/nick ") === 0) { | ||||
|             // Change user display name
 | ||||
|             promise = matrixService.setDisplayName($scope.textInput.substr(6)); | ||||
|         } | ||||
|         else { | ||||
|             promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput); | ||||
|         } | ||||
|  | @ -395,8 +377,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
| 
 | ||||
|         // Make recents highlight the current room
 | ||||
|         $scope.recentsSelectedRoomID = $scope.room_id; | ||||
|          | ||||
|                  | ||||
|         paginate(MESSAGES_PER_PAGINATION); | ||||
|          | ||||
|         updateMemberListPresenceAge(); | ||||
|     };  | ||||
|      | ||||
|     $scope.inviteUser = function(user_id) { | ||||
|  | @ -404,18 +388,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|         matrixService.invite($scope.room_id, user_id).then( | ||||
|             function() { | ||||
|                 console.log("Invited."); | ||||
|                 $scope.feedback = "Request for invitation succeeds"; | ||||
|                 $scope.feedback = "Invite sent successfully"; | ||||
|             }, | ||||
|             function(reason) { | ||||
|                 $scope.feedback = "Failure: " + reason; | ||||
|             }); | ||||
|     }; | ||||
| 
 | ||||
|     // Open the user profile page
 | ||||
|     $scope.goToUserPage = function(user_id) { | ||||
|         $location.url("/user/" + user_id); | ||||
|     }; | ||||
| 
 | ||||
|     $scope.leaveRoom = function() { | ||||
|          | ||||
|         matrixService.leave($scope.room_id).then( | ||||
|  | @ -487,7 +466,5 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) | |||
|     } | ||||
| 
 | ||||
|     $scope.onCallHangup = function() { | ||||
|         $scope.feedback = "Call ended"; | ||||
|         $scope.currentCall = undefined; | ||||
|     } | ||||
| }]); | ||||
|  |  | |||
|  | @ -1,13 +1,15 @@ | |||
| <div ng-controller="RoomController" data-ng-init="onInit()" class="room"> | ||||
|     <h1 id="roomLogo">[matrix]</h1> | ||||
| <div ng-controller="RoomController" data-ng-init="onInit()" class="room" style="height: 100%;"> | ||||
| 
 | ||||
|     <div id="page"> | ||||
|     <div id="wrapper"> | ||||
| 
 | ||||
|     <div id="roomName"> | ||||
|         {{ room_alias || room_id }} | ||||
|     <div id="roomHeader"> | ||||
|         <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> | ||||
|         <div id="roomName"> | ||||
|             {{ room_alias || room_id }} | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div id="roomPage"> | ||||
|     <div id="roomWrapper"> | ||||
|          | ||||
|     <div id="roomRecentsTableWrapper"> | ||||
|         <div ng-include="'recents/recents.html'"></div> | ||||
|     </div> | ||||
|  | @ -15,17 +17,17 @@ | |||
|     <div id="usersTableWrapper"> | ||||
|         <table id="usersTable"> | ||||
|             <tr ng-repeat="member in members | orderMembersList"> | ||||
|                 <td class="userAvatar mouse-pointer" ng-click="goToUserPage(member.id)"> | ||||
|                 <td class="userAvatar mouse-pointer" ng-click="$parent.goToUserPage(member.id)" ng-class="member.membership == 'invite' ? 'invited' : ''"> | ||||
|                     <img class="userAvatarImage"  | ||||
|                          ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}"  | ||||
|                          ng-src="{{member.avatar_url || 'img/default-profile.png'}}"  | ||||
|                          alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}" | ||||
|                          title="{{ member.id }}" | ||||
|                          width="80" height="80"/> | ||||
|                     <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/> | ||||
|                     <div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div> | ||||
|                 </td> | ||||
|                 <td class="userPresence" ng-class="member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')"> | ||||
|                     {{ member.mtime_age | duration }}<br/>{{ member.mtime_age ? "ago" : "" }} | ||||
|                 <td class="userPresence" ng-class="(member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')"> | ||||
|                     <span ng-show="member.mtime_age">{{ member.mtime_age + (now - member.last_updated) | duration }}<br/>ago</span> | ||||
|                 </td> | ||||
|         </table> | ||||
|     </div> | ||||
|  | @ -40,7 +42,7 @@ | |||
|                     <div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div> | ||||
|                 </td> | ||||
|                 <td class="avatar"> | ||||
|                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32" | ||||
|                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" | ||||
|                          ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/> | ||||
|                 </td> | ||||
|                 <td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'"> | ||||
|  | @ -64,7 +66,7 @@ | |||
|                     </div> | ||||
|                 </td> | ||||
|                 <td class="rightBlock"> | ||||
|                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32" | ||||
|                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" | ||||
|                          ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/> | ||||
|                 </td> | ||||
|             </tr> | ||||
|  | @ -86,12 +88,12 @@ | |||
|                     </td> | ||||
|                     <td id="buttonsCell"> | ||||
|                         <button ng-click="send()">Send</button> | ||||
|                         <button m-file-input="imageFileToSend">Image</button> | ||||
|                         <button m-file-input="imageFileToSend" class="extraControls">Image</button> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             </table> | ||||
| 
 | ||||
|             <div id="extraControls"> | ||||
|             <div class="extraControls"> | ||||
|                 <span> | ||||
|                    Invite a user:  | ||||
|                         <input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>      | ||||
|  | @ -105,6 +107,10 @@ | |||
|                 <button ng-click="hangupCall()">Reject</button> | ||||
|                 </div> | ||||
|                 <button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing'">Hang up</button> | ||||
|                 <span ng-show="currentCall.state == 'invite_sent'">Calling...</span> | ||||
|                 <span ng-show="currentCall.state == 'connecting'">Call Connecting...</span> | ||||
|                 <span ng-show="currentCall.state == 'connected'">Call Connected</span> | ||||
|                 <span ng-show="currentCall.state == 'ended'">Call Ended</span> | ||||
|                 <span style="display: none; ">{{ currentCall.state }}</span> | ||||
|             </div> | ||||
|          | ||||
|  |  | |||
|  | @ -22,8 +22,38 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu | |||
|     $scope.config = matrixService.config(); | ||||
| 
 | ||||
|     $scope.profile = { | ||||
|         displayName: $scope.config.displayName, | ||||
|         avatarUrl: $scope.config.avatarUrl | ||||
|         displayName: "", | ||||
|         avatarUrl: "" | ||||
|     }; | ||||
| 
 | ||||
|     // The profile as stored on the server
 | ||||
|     $scope.profileOnServer = { | ||||
|         displayName: "", | ||||
|         avatarUrl: "" | ||||
|     }; | ||||
| 
 | ||||
|     $scope.onInit = function() { | ||||
|         // Load profile data
 | ||||
|         // Display name
 | ||||
|         matrixService.getDisplayName($scope.config.user_id).then( | ||||
|             function(response) { | ||||
|                 $scope.profile.displayName = response.data.displayname; | ||||
|                 $scope.profileOnServer.displayName = response.data.displayname; | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Can't load display name"; | ||||
|             }  | ||||
|         ); | ||||
|         // Avatar
 | ||||
|         matrixService.getProfilePictureUrl($scope.config.user_id).then( | ||||
|             function(response) { | ||||
|                 $scope.profile.avatarUrl = response.data.avatar_url; | ||||
|                 $scope.profileOnServer.avatarUrl = response.data.avatar_url; | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Can't load avatar URL"; | ||||
|             }  | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     $scope.$watch("profile.avatarFile", function(newValue, oldValue) { | ||||
|  | @ -41,10 +71,10 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu | |||
|     }); | ||||
|      | ||||
|     $scope.saveProfile = function() { | ||||
|         if ($scope.profile.displayName !== $scope.config.displayName) { | ||||
|         if ($scope.profile.displayName !== $scope.profileOnServer.displayName) { | ||||
|             setDisplayName($scope.profile.displayName); | ||||
|         } | ||||
|         if ($scope.profile.avatarUrl !== $scope.config.avatarUrl) { | ||||
|         if ($scope.profile.avatarUrl !== $scope.profileOnServer.avatarUrl) { | ||||
|             setAvatar($scope.profile.avatarUrl); | ||||
|         } | ||||
|     }; | ||||
|  | @ -53,11 +83,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu | |||
|         matrixService.setDisplayName(displayName).then( | ||||
|             function(response) { | ||||
|                 $scope.feedback = "Updated display name."; | ||||
|                  | ||||
|                 var config = matrixService.config(); | ||||
|                 config.displayName = displayName; | ||||
|                 matrixService.setConfig(config); | ||||
|                 matrixService.saveConfig(); | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Can't update display name: " + error.data; | ||||
|  | @ -71,11 +96,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu | |||
|             function(response) { | ||||
|                 console.log("Updated avatar"); | ||||
|                 $scope.feedback = "Updated avatar."; | ||||
|                  | ||||
|                 var config = matrixService.config(); | ||||
|                 config.avatarUrl = avatarURL; | ||||
|                 matrixService.setConfig(config); | ||||
|                 matrixService.saveConfig(); | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Can't update avatar: " + error.data; | ||||
|  | @ -143,4 +163,23 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu | |||
|             } | ||||
|         ); | ||||
|     }; | ||||
|      | ||||
|      | ||||
|     /*** Desktop notifications section ***/ | ||||
|     $scope.settings = { | ||||
|         notifications: undefined | ||||
|     }; | ||||
| 
 | ||||
|     // If the browser supports it, check the desktop notification state
 | ||||
|     if ("Notification" in window) { | ||||
|         $scope.settings.notifications = window.Notification.permission; | ||||
|     } | ||||
| 
 | ||||
|     $scope.requestNotifications = function() { | ||||
|         console.log("requestNotifications"); | ||||
|         window.Notification.requestPermission(function (permission) { | ||||
|             console.log("   -> User decision: " + permission); | ||||
|             $scope.settings.notifications = permission; | ||||
|         }); | ||||
|     }; | ||||
| }]); | ||||
|  | @ -1,35 +1,29 @@ | |||
| <div ng-controller="SettingsController" class="user"> | ||||
| <div ng-controller="SettingsController" class="user" data-ng-init="onInit()"> | ||||
| 
 | ||||
|     <div id="page"> | ||||
|     <div id="wrapper"> | ||||
|          | ||||
|         <h3>Me</h3> | ||||
|         <div> | ||||
| 
 | ||||
|         <div id="genericHeading"> | ||||
|             <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> | ||||
|         </div> | ||||
| 
 | ||||
|         <h1>Settings</h1> | ||||
|         <div class="section"> | ||||
|             <form> | ||||
|                 <table> | ||||
|                     <tr> | ||||
|                         <td> | ||||
|                             <div class="profile-avatar"> | ||||
|                                 <img ng-src="{{ profile.avatarUrl || 'img/default-profile.jpg' }}" m-file-input="profile.avatarFile"/> | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td> | ||||
|                             <div id="user-ids"> | ||||
|                                 <input size="40" ng-model="profile.displayName" placeholder="Your name"/>             | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td> | ||||
|                             <button ng-disabled="(profile.displayName == config.displayName) && (profile.avatarUrl == config.avatarUrl)" | ||||
|                                     ng-click="saveProfile()">Save</button>     | ||||
|                         </td> | ||||
|                     </tr> | ||||
|                 </table> | ||||
|                 <div class="profile-avatar"> | ||||
|                     <img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}" m-file-input="profile.avatarFile"/> | ||||
|                 </div> | ||||
|                 <div id="user-ids"> | ||||
|                     <input size="40" ng-model="profile.displayName" placeholder="Your display name"/> | ||||
|                     <br/> | ||||
|                     <button ng-disabled="(profile.displayName == profileOnServer.displayName) && (profile.avatarUrl == profileOnServer.avatarUrl)" | ||||
|                             ng-click="saveProfile()">Save</button>     | ||||
|                 </div> | ||||
|             </form> | ||||
|         </div> | ||||
|         <br/> | ||||
| 
 | ||||
|         <h3>Linked emails</h3> | ||||
|         <div> | ||||
|         <div class="section"> | ||||
|             <form> | ||||
|                 <input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" /> | ||||
|                 <button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)"> | ||||
|  | @ -52,22 +46,35 @@ | |||
|             </table> | ||||
|         </div> | ||||
|         <br/> | ||||
| 
 | ||||
|         <h3>Configuration</h3> | ||||
|         <div> | ||||
|             <div>Home server: {{ config.homeserver }} </div> | ||||
|             <div>User ID: {{ config.user_id }} </div> | ||||
|             <div>Access token: {{ config.access_token }} </div> | ||||
|          | ||||
|         <h3>Desktop notifications</h3> | ||||
|         <div class="section" ng-switch="settings.notifications"> | ||||
|             <div ng-switch-when="granted"> | ||||
|                 Notifications are enabled. | ||||
|             </div> | ||||
|             <div ng-switch-when="denied"> | ||||
|                 You have denied permission for notifications.<br/> | ||||
|                 To enable it, reset the notification setting for this web site into your browser settings. | ||||
|             </div> | ||||
|             <div ng-switch-when="default"> | ||||
|                 <button ng-click="requestNotifications()" style="font-size: 14pt">Enable desktop notifications</button> | ||||
|             </div> | ||||
|             <div ng-switch-default=""> | ||||
|                 Sorry, your browser does not support notifications. | ||||
|             </div> | ||||
|         </div> | ||||
|         <br/> | ||||
|          | ||||
|         <div> | ||||
|             <div><button ng-click="requestNotifications()">Request notifications</button></div> | ||||
|         <h3>Configuration</h3> | ||||
|         <div class="section"> | ||||
|             <div>Home server: {{ config.homeserver }} </div> | ||||
|             <div>Identity server: {{ config.identityServer }} </div> | ||||
|             <div>User ID: {{ config.user_id }} </div> | ||||
|             <div>Access token: {{ config.access_token }} </div> | ||||
|         </div> | ||||
|         <br/> | ||||
| 
 | ||||
|         {{ feedback }} | ||||
| 
 | ||||
|     </div>     | ||||
|     </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -25,14 +25,42 @@ angular.module('UserController', ['matrixService']) | |||
|         avatar_url: undefined | ||||
|     }; | ||||
|      | ||||
|     $scope.user_id = matrixService.config().user_id; | ||||
|      | ||||
|     matrixService.getDisplayName($scope.user.id).then( | ||||
|         function(response) { | ||||
|             $scope.user.displayname = response.data.displayname; | ||||
|         } | ||||
|     );  | ||||
|      | ||||
|     matrixService.getProfilePictureUrl($scope.user.id).then( | ||||
|         function(response) { | ||||
|             $scope.user.avatar_url = response.data.avatar_url; | ||||
|         } | ||||
|     ); | ||||
|      | ||||
|     $scope.messageUser = function() {     | ||||
|          | ||||
|         // FIXME: create a new room every time, for now
 | ||||
|          | ||||
|         matrixService.create(null, 'private').then( | ||||
|             function(response) {  | ||||
|                 // This room has been created. Refresh the rooms list
 | ||||
|                 var room_id = response.data.room_id; | ||||
|                 console.log("Created room with id: "+ room_id); | ||||
|                  | ||||
|                 matrixService.invite(room_id, $scope.user.id).then( | ||||
|                     function() { | ||||
|                         $scope.feedback = "Invite sent successfully"; | ||||
|                         $scope.$parent.goToPage("/room/" + room_id); | ||||
|                     }, | ||||
|                     function(reason) { | ||||
|                         $scope.feedback = "Failure: " + JSON.stringify(reason); | ||||
|                     }); | ||||
|             }, | ||||
|             function(error) { | ||||
|                 $scope.feedback = "Failure: " + JSON.stringify(error.data); | ||||
|             });                 | ||||
|     }; | ||||
|      | ||||
| }]); | ||||
|  | @ -1,31 +1,25 @@ | |||
| <div ng-controller="UserController" class="user"> | ||||
|     <h1 id="logo">[matrix]</h1> | ||||
| 
 | ||||
|     <div id="page"> | ||||
|     <div id="wrapper"> | ||||
|          | ||||
|         <div> | ||||
|             <form> | ||||
|                 <table> | ||||
|                     <tr> | ||||
|                         <td> | ||||
|                             <div class="profile-avatar"> | ||||
|                                 <img ng-src="{{ user.avatar_url || 'img/default-profile.jpg' }}"/> | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td> | ||||
|                             <div id="user-ids"> | ||||
|                                 <div id="user-displayname">{{ user.displayname }}</div> | ||||
|                                 <div>{{ user.id }}</div>                         | ||||
|                             </div> | ||||
|                         </td> | ||||
|                     </tr> | ||||
|                 </table> | ||||
|             </form> | ||||
| 
 | ||||
|         <div id="genericHeading"> | ||||
|             <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a> | ||||
|         </div> | ||||
| 
 | ||||
|         <h1>{{ user.displayname || user.id }}</h1> | ||||
| 
 | ||||
|         <div> | ||||
|             <div class="profile-avatar"> | ||||
|                 <img ng-src="{{ user.avatar_url || 'img/default-profile.png' }}"/> | ||||
|             </div> | ||||
|             <div id="user-ids"> | ||||
|                 <div>{{ user.id }}</div>                         | ||||
|             </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <button ng-hide="user.id == user_id" ng-click="messageUser()" style="font-size: 14pt; margin-top: 40px; margin-bottom: 40px">Start chat</button> | ||||
|         <br/> | ||||
|         {{ feedback }} | ||||
| 
 | ||||
|     </div>     | ||||
|     </div> | ||||
| </div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Erik Johnston
						Erik Johnston