Merge branch 'master' into human-id-rules
commit
0131543f00
@ -1,303 +0,0 @@
|
||||
#1: Lightweight IM client (no perm storage)
|
||||
#2: Mobile IM client (perm storage)
|
||||
#3: MIDI client
|
||||
#4: Animatrix client
|
||||
#5: Unity object trees
|
||||
#6: Forum
|
||||
#7: Social Network ("Walls", PMs, groups)
|
||||
#8: Minecraft-clone
|
||||
#9: Bug Tracking Software
|
||||
#10: Global 'Like' widget, which links through to a room.
|
||||
|
||||
================
|
||||
|
||||
#1: Lightweight IM client (no perm storage)
|
||||
-------------------------------------------
|
||||
Description:
|
||||
An IM client (think web client) with no way of persisting data beyond
|
||||
a session (the instance a person is using the app).
|
||||
Features:
|
||||
Recent activity, Room screen (member list, etc), User page, just like
|
||||
the web client.
|
||||
Actions:
|
||||
- Send a one-to-one message to someone.
|
||||
- Accept an invite.
|
||||
- Populate recent activity (all rooms joined with latest message + room names/aliases, rooms invited to + room names/aliases)
|
||||
- Populate scrollback if click on room
|
||||
- Populate member list if click on room + get presence updates for them
|
||||
- Populate room name / topic if click on room
|
||||
- Create an empty room.
|
||||
- Join a room from an alias.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Send a one-to-one message to someone.
|
||||
How:
|
||||
Enter their username and hit Message. Taken to room page with invited user.
|
||||
History displays that I've invited someone / joined the room. Enter a message
|
||||
and hit send. Message appears in window.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
-- @user:domain -->
|
||||
<--- room ID, ACK--
|
||||
<-historical msgs--
|
||||
-- msg,room ID --->
|
||||
<--- ACK ----------
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Accept an invite.
|
||||
How:
|
||||
Get list of invites. Click one of them to 'accept' it. May or may not want
|
||||
room content.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- req invites ->
|
||||
<--- [inv,inv] ----
|
||||
---- accept inv -->
|
||||
<--- ACK ----------
|
||||
<--- room content-- (optional)
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Populate recent activity (all rooms joined with latest message + room names/aliases, rooms invited to + room names/aliases)
|
||||
How:
|
||||
Request joined rooms with latest message and room name. Request rooms invited to. Possibly extra info like # joined members.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- req sync ---->
|
||||
<---joined rooms--- {msg,name,alias,#members?}
|
||||
<---invited rooms-- {name,alias}
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Populate scrollback if click on room.
|
||||
How:
|
||||
Request scrollback for room.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- room id ----->
|
||||
<--- scrollback ---
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Populate member list if click on room + get presence updates for them.
|
||||
How:
|
||||
Click on room. Member list with names/presence/pics appears. May not want
|
||||
pic.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- req mem list ->
|
||||
<--- members ------- {name,pic,presence}
|
||||
- monitor presence->
|
||||
...
|
||||
<- presence change--
|
||||
<- presence change--
|
||||
...
|
||||
-- stop presence --->
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Populate room name / topic if click on room.
|
||||
How:
|
||||
Click on room. Room name and topic with aliases appears. May not want topic
|
||||
(eg screen size).
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- req room info->
|
||||
<--- room info ----- {name,topic,aliases}
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Create an empty room.
|
||||
How:
|
||||
Type in room config (desired name, public/private, etc). Hit Create. Room is
|
||||
created. Possibly get room info.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- mkroom{config}->
|
||||
<--ACK{room_id}------
|
||||
<-- room info ------- (optional)
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Join a room from an alias.
|
||||
How:
|
||||
Type in alias. Hit Join. Room is joined. Possibly get room info.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
-- join{alias} ----->
|
||||
<--ACK{room_id}------
|
||||
<--room info--------- (optional)
|
||||
|
||||
|
||||
===========================
|
||||
|
||||
#2: Mobile IM client (perm storage)
|
||||
-----------------------------------
|
||||
Description:
|
||||
An IM client (think android/ios) which persists data on a database.
|
||||
Features:
|
||||
Recent activity, Room screen (member list, etc), User page, just like
|
||||
the web client.
|
||||
Actions:
|
||||
- Send a one-to-one message to someone.
|
||||
- Accept a stored invite.
|
||||
- Populate recent activity (all rooms joined with latest message + room names/aliases, rooms invited to + room names/aliases)
|
||||
- Populate scrollback if click on room
|
||||
- Populate member list if click on room + get presence updates for them
|
||||
- Populate room name / topic if click on room
|
||||
- Create an empty room.
|
||||
- Join a room from an alias.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Send a one-to-one message to someone (single room).
|
||||
How:
|
||||
Enter their username and hit Message. Taken to room page with invited user if no room exists,
|
||||
else takes to existing room. History displays that I've invited someone or scrollback. Enter
|
||||
a message and hit send. Message appears in window.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
-- @user:domain -->
|
||||
<--- room ID, ACK--
|
||||
<-historical msgs-- (optional; not if existing room)
|
||||
-- msg,room ID --->
|
||||
<--- ACK ----------
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Accept a stored invite.
|
||||
How:
|
||||
Send invite to server. Get room content (or NO-OP if already joined).
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- accept inv -->
|
||||
<--- ACK ----------
|
||||
<--- room content-- (optional)
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Populate recent activity (all rooms joined with latest message + room names/aliases, rooms invited to + room names/aliases)
|
||||
incrementally.
|
||||
How:
|
||||
Request recent activity diff. Get updated msg/name/#members for changed values only.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
- req sync{token}->
|
||||
<---diff{rooms}---- {msg,name,alias,#members?}
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Populate scrollback if click on room.
|
||||
How:
|
||||
Request scrollback for room. Either a diff or a page of scrollback
|
||||
depending on cached data.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
-room id{latest event}-> {max msgs}
|
||||
<--- scrollback -------- {fresh/incremental flag}
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Populate member list if click on room + get presence updates for them.
|
||||
How:
|
||||
Click on room. Member list with names/presence/pics appears. May not want
|
||||
pic.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- req mem list ->
|
||||
<--- members ------- {name,pic,presence}
|
||||
- monitor presence->
|
||||
...
|
||||
<- presence change--
|
||||
<- presence change--
|
||||
...
|
||||
-- stop presence --->
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Populate room name / topic if click on room.
|
||||
How:
|
||||
Click on room. Room name and topic with aliases appears. May not want topic
|
||||
(eg screen size). Display cached info until updated.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- req room info->
|
||||
<--- room info ----- {name,topic,aliases}
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Create an empty room.
|
||||
How:
|
||||
Type in room config (desired name, public/private, etc). Hit Create. Room is
|
||||
created. Possibly get room info.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
---- mkroom{config}->
|
||||
<--ACK{room_id}------
|
||||
<-- room info ------- (optional)
|
||||
|
||||
---
|
||||
|
||||
Action:
|
||||
Join a room from an alias.
|
||||
How:
|
||||
Type in alias. Hit Join. Room is joined. Possibly get room info.
|
||||
|
||||
::
|
||||
|
||||
Client Server
|
||||
-- join{alias} ----->
|
||||
<--ACK{room_id}------
|
||||
<--room info--------- (optional)
|
||||
|
||||
|
||||
|
@ -0,0 +1,222 @@
|
||||
Data flows for use cases
|
||||
========================
|
||||
|
||||
::
|
||||
|
||||
<- Data from server to client
|
||||
-> Data from client to server
|
||||
|
||||
Instant Messaging
|
||||
-----------------
|
||||
|
||||
Without storage
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
Home screen
|
||||
Data required on load:
|
||||
<- For each room the user is joined: Name, topic, # members, last message, room ID, aliases
|
||||
Data required when new message arrives for a room:
|
||||
<- Room ID, message content, sender (user ID, display name, avatar url)
|
||||
Data required when someone invites you to a room:
|
||||
<- Room ID, sender (user ID, display name, avatar url), Room Name, Room Topic
|
||||
Data required when you leave a room on another device:
|
||||
<- Room ID
|
||||
Data required when you join a room on another device:
|
||||
<- Name, topic, # members, last message, room ID, aliases
|
||||
Data required when your profile info changes on another device:
|
||||
<- new profile info e.g. avatar, display name, etc.
|
||||
|
||||
Creating a room
|
||||
-> Invitee list of user IDs, public/private, name of room, alias of room, topic of room
|
||||
<- Room ID
|
||||
|
||||
Joining a room (and dumped into chat screen on success)
|
||||
-> Room ID / Room alias
|
||||
<- Room ID, Room aliases (plural), Name, topic, member list (f.e. member: user ID,
|
||||
avatar, presence, display name, power level, whether they are typing), enough
|
||||
messages to fill screen (and whether there are more)
|
||||
|
||||
Chat Screen
|
||||
Data required when member name changes:
|
||||
<- new name, room ID, user ID, when in the context of the room did this occur
|
||||
Data required when the room name changes:
|
||||
<- new name, room ID, old room name?
|
||||
Invite a user:
|
||||
-> user ID, room ID
|
||||
<- display name / avatar of user invited (if known)
|
||||
Kick a user:
|
||||
-> user ID, room ID
|
||||
<- what message it came after
|
||||
Leave a room:
|
||||
-> room ID
|
||||
<- what message it came after
|
||||
|
||||
Send a message
|
||||
-> Message content, room ID, message sequencing (eg sending my 1st, 2nd, 3rd msg)
|
||||
<- actual content sent (if server mods it), what message it comes after (to correctly
|
||||
display the local echo)
|
||||
|
||||
Place a call (receive a call is just reverse)
|
||||
<- turn servers
|
||||
-> SDP offer
|
||||
-> Ice candidates (1 by 1; trickling)
|
||||
<- SDP answer
|
||||
<- Ice candidates
|
||||
|
||||
Scrolling back (infinite scrolling)
|
||||
-> Identifier for the earliest message, # requested messages
|
||||
<- requested messages (f.e change in display name, what the old name was), whether
|
||||
there are more.
|
||||
|
||||
|
||||
With storage
|
||||
~~~~~~~~~~~~
|
||||
::
|
||||
|
||||
Home Screen
|
||||
On Load
|
||||
-> Identifier which tells the server the client's current state (which rooms it is aware
|
||||
of, which messages it has, what display names for users, etc..)
|
||||
<- A delta from the client's current state to the current state on the server (e.g. the
|
||||
new rooms, the *latest* message if different, the changed display names, the new
|
||||
invites, etc). f.e Room: Whether the cache of the room that you have has been replaced
|
||||
with this new state.
|
||||
|
||||
Pre-load optimisation (not essential for this screen)
|
||||
-> Number of desired messages f.e room to cache
|
||||
<- f.e Room: the delta OR the entire state
|
||||
|
||||
|
||||
Bug Tracking
|
||||
------------
|
||||
::
|
||||
|
||||
Landing Page
|
||||
On Load
|
||||
<- Issues assigned to me, Issues I'm watching, Recent activity on other issues includes
|
||||
comments, list of projects
|
||||
|
||||
Search for an issue (assume text)
|
||||
-> Search string
|
||||
<- List of paginated issues
|
||||
Request page 2:
|
||||
-> Page number requested
|
||||
<- Page of paginated issues
|
||||
|
||||
Issue Page
|
||||
On Load
|
||||
-> Issue ID and Project ID (equiv to Room)
|
||||
<- Issue contents e.g. priority, resolution state, etc. All comments e.g. user ID,
|
||||
comment text, timestamp. Entire issue history e.g. changes in priority
|
||||
|
||||
Post a comment
|
||||
-> Issue ID, comment content, Project ID (equiv to Room)
|
||||
<- actual content sent (if modded), what comment it comes after
|
||||
|
||||
Set issue priority
|
||||
-> Issue ID, Project ID, desired priority
|
||||
<- What action in the history it came after
|
||||
|
||||
Someone else sets issue priority
|
||||
<- Issue ID, Project ID, new priority, where in the history
|
||||
|
||||
|
||||
Mapping model use cases to matrix models (Room, Message, etc)
|
||||
=============================================================
|
||||
|
||||
To think about:
|
||||
- Do we want to support the idea of forking off new rooms from existing ones? This
|
||||
and forums could benefit from it.
|
||||
|
||||
Bug tracking UI
|
||||
---------------
|
||||
::
|
||||
|
||||
Projects => Rooms
|
||||
Issues => Message Events
|
||||
Comments => Message Events (relates_to key)
|
||||
|
||||
Projects:
|
||||
- Unlikely that there will be 100,000s of issues, so having to pull in all the issues for a project is okay.
|
||||
- Permissions are usually per project and this Just Works.
|
||||
- New issues come in automatically and Just Work.
|
||||
- Can have read-only members
|
||||
|
||||
Issues:
|
||||
- Don't really want 1 Room per Issue, else you can have thousands of Rooms PER PROJECT, hence choice for
|
||||
Issues as Messages. Don't need to join a room for each issue.
|
||||
- Idea of issue owner is clear (sender of the message)
|
||||
- Updating issues requires an additional event similar to comments (with ``relates_to``)? Could possibly
|
||||
be state events? Don't really want all the history if say the priority was changed 1000 times, just want
|
||||
the current state of the key.
|
||||
|
||||
Comments:
|
||||
- Additional event with ``relates_to`` key.
|
||||
|
||||
|
||||
Forum
|
||||
-----
|
||||
::
|
||||
|
||||
Forum => Room (with pointers to Board Rooms)
|
||||
Boards => Room (with pointers to Thread Rooms)
|
||||
Threads => Room
|
||||
Messages => Message Events
|
||||
|
||||
Forum:
|
||||
- Contains 10s of Boards.
|
||||
- Contains special Message Events which point to different rooms f.e Board.
|
||||
|
||||
Boards:
|
||||
- Contains 100s of Threads.
|
||||
- Contains special Message Events which point to different rooms f.e. Thread.
|
||||
|
||||
Threads:
|
||||
- Contains 100s of Messages.
|
||||
|
||||
Can't do this nicely with the current Federation API because you have loads of
|
||||
Rooms and what does posting a message look like? Creating a thread is done by..?
|
||||
The user who is posting cannot create the thread because otherwise they would be
|
||||
the room creator and have ultimate privileges. So it has to be created by a bot
|
||||
of some kind which ties into auth (Application services?). To follow a board,
|
||||
you need a bot to join the Board Room and then watch it for changes...
|
||||
|
||||
Fundamental problem with forums is that there is only 1 PDU graph per room and
|
||||
you either have to pull in lots of graphs separately or one graph and filter it
|
||||
separately to get to the desired sub set of data. You have to subscribe into a
|
||||
lot of graphs if you subscribe to a board... If you have the entire board...
|
||||
good luck scrollbacking a particular thread.
|
||||
|
||||
|
||||
Google+ Community
|
||||
-----------------
|
||||
::
|
||||
|
||||
Community => Room (with pointers to Category Rooms)
|
||||
Category => Room
|
||||
Post => Message Events
|
||||
Comment => Message Events (relates_to key)
|
||||
|
||||
Community:
|
||||
- Contains 10s of categories.
|
||||
- Contains special Message Events which point to different rooms f.e Category.
|
||||
- Moderators of the community are mods in this room. They are in charge of making
|
||||
new categories and the subsequent rooms. Can get a bit funky if a mod creates a
|
||||
category room without the same permissions as the community room... but another
|
||||
mod can always delete the pointer to the buggy category room and make a new one.
|
||||
- Do we want to support the idea of forking off new rooms from existing ones? This
|
||||
and forums could benefit from it.
|
||||
|
||||
Category:
|
||||
- Contains 1000s of posts.
|
||||
- Same permissions as the community room. How to enforce? Fork off the community
|
||||
room?
|
||||
|
||||
Posts:
|
||||
- Contains 10s of comments.
|
||||
|
||||
This is similar to forums but you can more reasonably say "screw it, pull in the
|
||||
entire community of posts."
|
||||
|
@ -0,0 +1,14 @@
|
||||
Matrix Spec Design Workflow
|
||||
===========================
|
||||
|
||||
1. Write use cases
|
||||
|
||||
2. Design data flows for use cases
|
||||
|
||||
3. Design generic API (factoring out commonalities where possible)
|
||||
|
||||
4. Design transport-specific API with justifications
|
||||
|
||||
5. Formalise transport-specific API as swagger or similar
|
||||
|
||||
6. Evolve the generic API design doc and transport-specific API into the actual spec.
|
@ -0,0 +1,408 @@
|
||||
Federation
|
||||
==========
|
||||
.. sectnum::
|
||||
.. contents:: Table of Contents
|
||||
|
||||
Authorization
|
||||
-------------
|
||||
|
||||
When receiving new events from remote servers, or creating new events, a server
|
||||
must know whether that event is allowed by the authorization rules. These rules
|
||||
depend solely on the state at that event. The types of state events that affect
|
||||
authorization are:
|
||||
|
||||
- ``m.room.create``
|
||||
- ``m.room.member``
|
||||
- ``m.room.join_rules``
|
||||
- ``m.room.power_levels``
|
||||
|
||||
Servers should not create new events that reference unauthorized events.
|
||||
However, any event that does reference an unauthorized event is not itself
|
||||
automatically considered unauthorized.
|
||||
|
||||
Unauthorized events that appear in the event graph do *not* have any effect on
|
||||
the state of the graph.
|
||||
|
||||
.. Note:: This is in contrast to redacted events which can still affect the
|
||||
state of the graph. For example, a redacted *"join"* event will still
|
||||
result in the user being considered joined.
|
||||
|
||||
|
||||
Rules
|
||||
~~~~~
|
||||
|
||||
The following are the rules to determine if an event is authorized (this does
|
||||
include validation).
|
||||
|
||||
**TODO**: What signatures do we expect?
|
||||
|
||||
1. If type is ``m.room.create`` allow if and only if it has no prev events.
|
||||
#. If type is ``m.room.member``:
|
||||
|
||||
a. If ``membership`` is ``join``:
|
||||
|
||||
i. If the previous event is an ``m.room.create``, the depth is 1 and
|
||||
the ``state_key`` is the creator, then allow.
|
||||
#. If the ``state_key`` does not match ``sender`` key, reject.
|
||||
#. If the current state has ``membership`` set to ``join``.
|
||||
#. If the ``sender`` is in the ``m.room.may_join`` list. [Not currently
|
||||
implemented]
|
||||
#. If the ``join_rules`` is:
|
||||
|
||||
- ``public``: allow.
|
||||
- ``invite``: allow if the current state has ``membership`` set to
|
||||
``invite``
|
||||
- ``knock``: **TODO**.
|
||||
- ``private``: Reject.
|
||||
|
||||
#. Reject
|
||||
|
||||
#. If ``membership`` is ``invite`` then allow if ``sender`` is in room,
|
||||
otherwise reject.
|
||||
#. If ``membership`` is ``leave``:
|
||||
|
||||
i. If ``sender`` matches ``state_key`` allow.
|
||||
#. If ``sender``'s power level is greater than the the ``kick_level``
|
||||
given in the current ``m.room.power_levels`` state (defaults to 50),
|
||||
and the ``state_key``'s power level is less than or equal to the
|
||||
``sender``'s power level, then allow.
|
||||
#. Reject.
|
||||
|
||||
#. If ``membership`` is ``ban``:
|
||||
|
||||
i. **TODO**.
|
||||
|
||||
#. Reject.
|
||||
|
||||
#. Reject the event if the event type's required power level is less that the
|
||||
``sender``'s power level.
|
||||
#. If the ``sender`` is not in the room, reject.
|
||||
#. If the type is ``m.room.power_levels``:
|
||||
|
||||
a. **TODO**.
|
||||
|
||||
#. Allow.
|
||||
|
||||
|
||||
Definitions
|
||||
~~~~~~~~~~~
|
||||
|
||||
Required Power Level
|
||||
A given event type has an associated *required power level*. This is given
|
||||
by the current ``m.room.power_levels`` event, it is either listed explicitly
|
||||
in the ``events`` section or given by either ``state_default`` or
|
||||
``events_default`` depending on if the event type is a state event or not.
|
||||
|
||||
|
||||
Auth events
|
||||
~~~~~~~~~~~
|
||||
|
||||
The auth events of an event are the set of events used by the authorization
|
||||
algorithm to accept the event. These should be a subset of the current state.
|
||||
|
||||
A server is required to store the complete chain of auth events for all events
|
||||
it serves to remote servers.
|
||||
|
||||
All auth events have type:
|
||||
|
||||
- ``m.room.create``
|
||||
- ``m.room.power_levels``
|
||||
- ``m.room.member``
|
||||
|
||||
.. todo
|
||||
We probably should probably give a lower band of how long auth events
|
||||
should be kept around for.
|
||||
|
||||
Auth chain
|
||||
~~~~~~~~~~
|
||||
|
||||
The *auth chain* for an event is the recursive list of auth events and the auth
|
||||
chain for those auth events.
|
||||
|
||||
.. Note:: The auth chain for an event gives all the information a server needs
|
||||
to accept an event. However, being given an auth chain for an event
|
||||
that appears valid does not mean that the event might not later be
|
||||
rejected. For example if we discover that the sender had been banned
|
||||
between the join event listed in the auth events and the event being
|
||||
authed.
|
||||
|
||||
**TODO**: Clean the above explanations up a bit.
|
||||
|
||||
|
||||
Auth chain resolution
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If an auth check fails, or if we get told something we accepted should have
|
||||
been rejected, we need to try and determine who is right.
|
||||
|
||||
If two servers disagree about the validity of the auth events, both should
|
||||
inform the other of what they think the current auth chain is. If either are
|
||||
missing auth events that they know are valid (through authorization and state
|
||||
resolution) they process the missing events as usual.
|
||||
|
||||
If either side notice that the other has accepted an auth events we think
|
||||
should be rejected (for reasons *not* in their auth chain), that server should
|
||||
inform the other with suitable proof.
|
||||
|
||||
The proofs can be:
|
||||
|
||||
- An *event chain* that shows an auth event is *not* an ancestor of the event.
|
||||
This can be done by giving the full ancestor chains up to the depth of the
|
||||
invalid auth event.
|
||||
- Given an event (and event chain?) showing that authorization had been revoked.
|
||||
|
||||
If a server discovers it cannot prove the other side is wrong, then it accepts
|
||||
that the other is correct; i.e. we always accept that the other side is correct
|
||||
unless we can prove otherwise.
|
||||
|
||||
|
||||
|
||||
State Resolution
|
||||
----------------
|
||||
|
||||
**TODO**
|
||||
|
||||
When two branches in the event graph merge, the state of those branches might
|
||||
differ, so a *state resolution* algorithm must be used to determine the current
|
||||
state of the resultant merge.
|
||||
|
||||
The properties of the state resolution algorithm are:
|
||||
|
||||
- Must only depend on the event graph, and not local server state.
|
||||
- When two state events are comparable, the descendant one should be picked.
|
||||
- Must not require the full event graph.
|
||||
|
||||
The following algorithm satisfies these requirements; given two or more events,
|
||||
pick the one with the greatest:
|
||||
|
||||
#. Depth.
|
||||
#. Hash of event_id.
|
||||
|
||||
|
||||
This works except in the case of auth events, where we need to mitigate against
|
||||
the attack where servers artificially netsplit to avoid bans or power level
|
||||
changes.
|
||||
|
||||
We want the following rules to apply:
|
||||
|
||||
#. If power levels have been changed on two different branches use the rules
|
||||
above, ensuring that the one picked is a valid change from the one not picked.
|
||||
#. Similarly handle membership changes (e.g. bans, kicks, etc.)
|
||||
#. Any state merged must be allowed by the newly merged auth events. If none of
|
||||
the candidate events for a given state are allowed, we pick the last event
|
||||
given by the ordering above (i.e. we pick one with the least depth).
|
||||
|
||||
|
||||
|
||||
State Conflict Resolution
|
||||
-------------------------
|
||||
|
||||
If a server discovers that it disagrees with another about the current state,
|
||||
it can follow the same process outlined in *Auth chain resolution* to resolve
|
||||
these conflicts.
|
||||
|
||||
Constructing a new event
|
||||
------------------------
|
||||
|
||||
**TODO**
|
||||
|
||||
When constructing a new event, the server should insert the following fields:
|
||||
|
||||
- ``prev_events``: The list of event ids of what the server believes are the
|
||||
current leaf nodes of the event graph (i.e., nodes that have been received
|
||||
but are yet to be referenced by another event).
|
||||
- ``depth``: An integer one greater than the maximum depth of the event's
|
||||
previous events.
|
||||
- ``auth_events``: The list of event ids that authorizes this event. This
|
||||
should be a subset of the current state.
|
||||
- ``origin_server_ts``: The time the server created the event.
|
||||
- ``origin``: The name of the server.
|
||||
|
||||
|
||||
Signing and Hashes
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**TODO**
|
||||
|
||||
Validation
|
||||
----------
|
||||
|
||||
**TODO**
|
||||
|
||||
Domain specific string
|
||||
A string of the form ``<prefix><localpart>:<domain>``, where <prefix> is a
|
||||
single character, ``<localpart>`` is an arbitrary string that does not
|
||||
include a colon, and `<domain>` is a valid server name.
|
||||
|
||||
``room_id``
|
||||
A domain specific string with prefix ``!`` that is static across all events
|
||||
in a graph and uniquely identifies it. The ``domain`` should be that of the
|
||||
home server that created the room (i.e., the server that generated the
|
||||
first ``m.room.create`` event).
|
||||
|
||||
``sender``
|
||||
The entity that logically sent the event. This is usually a user id, but
|
||||
can also be a server name.
|
||||
|
||||
User Id
|
||||
A domain specific string with prefix ``@`` representing a user account. The
|
||||
``domain`` is the home server of the user and is the server used to contact
|
||||
the user.
|
||||
|
||||
Joining a room
|
||||
--------------
|
||||
|
||||
If a user requests to join a room that the server is already in (i.e. the a
|
||||
user on that server has already joined the room) then the server can simply
|
||||
generate a join event and send it as normal.
|
||||
|
||||
If the server is not already in the room it needs to will need to join via
|
||||
another server that is already in the room. This is done as a two step process.
|
||||
|
||||
First, the local server requests from the remote server a skeleton of a join
|
||||
event. The remote does this as the local server does not have the event graph
|
||||
to use to fill out the ``prev_events`` key in the new event. Critically, the
|
||||
remote server does not process the event it responded with.
|
||||
|
||||
Once the local server has this event, it fills it out with any extra data and
|
||||
signs it. Once ready the local server sends this event to a remote server
|
||||
(which could be the same or different from the first remote server), this
|
||||
remote server then processes the event and distributes to all the other
|
||||
participating servers in that room. The local server is told about the
|
||||
current state and complete auth chain for the join event. The local server
|
||||
can then process the join event itself.
|
||||
|
||||
|
||||
.. Note::
|
||||
Finding which server to use to join any particular room is not specified.
|
||||
|
||||
|
||||
Inviting a user
|
||||
---------------
|
||||
|
||||
To invite a remote user to a room we need their home server to sign the invite
|
||||
event. This is done by sending the event to the remote server, which then signs
|
||||
the event, before distributing the invite to other servers.
|
||||
|
||||
|
||||
Handling incoming events
|
||||
------------------------
|
||||
|
||||
When a server receives an event, it should:
|
||||
|
||||
#. Check if it knows about the room. If it doesn't, then it should get the
|
||||
current state and auth events to determine whether the server *should* be in
|
||||
the room. If so continue, if not drop or reject the event
|
||||
#. If the server already knew about the room, check the prev events to see if
|
||||
it is missing any events. If it is, request them. Servers should limit how
|
||||
far back they will walk the event graph for missing events.
|
||||
#. If the server does not have all the prev events, then it should request the
|
||||
current state and auth events from a server.
|
||||
|
||||
|
||||
Failures
|
||||
--------
|
||||
|
||||
A server can notify a remote server about something it thinks it has done
|
||||
wrong using the failures mechanism. For example, the remote accepted an event
|
||||
the local think it shouldn't have.
|
||||
|
||||
A failure has a severity level depending on the action taken by the local
|
||||
server. These levels are:
|
||||
|
||||
``FATAL``
|
||||
The local server could not parse the event, for example due to a missing
|
||||
required field.
|
||||
|
||||
``ERROR``
|
||||
The local server *could* parse the event, but it was rejected. For example,
|
||||
the event may have failed an authorization check.
|
||||
|
||||
``WARN``
|
||||
The local server accepted the event, but something was unexpected about it.
|
||||
For example, the event may have referenced another event the local server
|
||||
thought should be rejected.
|
||||
|
||||
A failure also includes several other fields:
|
||||
|
||||
``code``
|
||||
A numeric code (to be defined later) indicating a particular type of
|
||||
failure.
|
||||
|
||||
``reason``
|
||||
A short string indicating what was wrong, for diagnosis purposes on the
|
||||
remote server.
|
||||
|
||||
``affected``
|
||||
The event id of the event this failure is responding to. For example, if
|
||||
an accepted event referenced a rejected event, this would point to the
|
||||
accepted one.
|
||||
|
||||
``source``
|
||||
The event id of the event that was the source of this unexpected behaviour.
|
||||
For example, if an accepted event referenced a rejected event, this would
|
||||
point to the rejected one.
|
||||
|
||||
|
||||
Appendix
|
||||
========
|
||||
|
||||
**TODO**
|
||||
|
||||
Example event:
|
||||
|
||||
.. code::
|
||||
|
||||
{
|
||||
"auth_events": [
|
||||
[
|
||||
"$14187571482fLeia:localhost:8480",
|
||||
{
|
||||
"sha256": "kiZUclzzPetHfy0rVoYKnYXnIv5VxH8a4996zVl8xbw"
|
||||
}
|
||||
],
|
||||
[
|
||||
"$14187571480odWTd:localhost:8480",
|
||||
{
|
||||
"sha256": "GqtndjviW9yPGaZ6EJfzuqVCRg5Lhoyo4YYv1NFP7fw"
|
||||
}
|
||||
],
|
||||
[
|
||||
"$14205549830rrMar:localhost:8480",
|
||||
{
|
||||
"sha256": "gZmL23QdWjNOmghEZU6YjqgHHrf2fxarKO2z5ZTbkig"
|
||||
}
|
||||
]
|
||||
],
|
||||
"content": {
|
||||
"body": "Test!",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"depth": 250,
|
||||
"event_id": "$14207181140uTFlx:localhost:8480",
|
||||
"hashes": {
|
||||
"sha256": "k1nuafFdFvZXzhb5NeTE0Q2Jkqu3E8zkh3uH3mqwIxc"
|
||||
},
|
||||
"origin": "localhost:8480",
|
||||
"origin_server_ts": 1420718114694,
|
||||
"prev_events": [
|
||||
[
|
||||
"$142071809077XNNkP:localhost:8480",
|
||||
{
|
||||
"sha256": "xOnU1b+4LOVz5qih0dkNFrdMgUcf35fKx9sdl/gqhjY"
|
||||
}
|
||||
]
|
||||
],
|
||||
"room_id": "!dwZDafgDEFTtpPKpLy:localhost:8480",
|
||||
"sender": "@bob:localhost:8480",
|
||||
"signatures": {
|
||||
"localhost:8480": {
|
||||
"ed25519:auto": "Nzd3D+emFBJJ4LCTzQEZaKO0Sa3sSTR1fGpu8OWXYn+7XUqke9Q1jYUewrEfxb3lPxlYWm/GztVUJizLz1K5Aw"
|
||||
}
|
||||
},
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 500
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,123 @@
|
||||
# JSON Schema for Matrix events. http://json-schema.org/
|
||||
title: "Matrix Event"
|
||||
type: object
|
||||
properties:
|
||||
auth_events:
|
||||
description: The events needed to authenticate this event
|
||||
$ref: "#/definitions/event_reference_list"
|
||||
content:
|
||||
type: object
|
||||
description: The body of the event.
|
||||
depth:
|
||||
type: integer
|
||||
description: A number one greater than that of any preceeding event.
|
||||
minimum: 0
|
||||
event_id:
|
||||
$ref: "#/definitions/event_id"
|
||||
hashes:
|
||||
$ref: "#/definitions/hashes"
|
||||
origin:
|
||||
$ref: "#/definitions/server_id"
|
||||
origin_server_ts:
|
||||
type: integer
|
||||
description: Posix timestamp on the originating server
|
||||
minimum: 0
|
||||
prev_events:
|
||||
description: The event(s) this event came after.
|
||||
$ref: "#/definitions/event_reference_list"
|
||||
prev_state:
|
||||
description: The state event(s) this event replaces.
|
||||
$ref: "#/definitions/event_reference_list"
|
||||
room_id:
|
||||
$ref: "#/definitions/room_id"
|
||||
sender:
|
||||
oneOf:
|
||||
- $ref: "#/definitions/user_id"
|
||||
- $ref: "#/definitions/server_id"
|
||||
state_key:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
unsigned:
|
||||
type: object
|
||||
|
||||
required:
|
||||
- auth_events
|
||||
- content
|
||||
- depth
|
||||
- event_id
|
||||
- hashes
|
||||
- origin
|
||||
- origin_server_ts
|
||||
- prev_events
|
||||
- room_id
|
||||
- sender
|
||||
- type
|
||||
|
||||
dependencies:
|
||||
state_key:
|
||||
- prev_state
|
||||
prev_state:
|
||||
- state_key
|
||||
|
||||
definitions:
|
||||
server_id:
|
||||
type: string
|
||||
description: Identifies a server.
|
||||
pattern: "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*\
|
||||
([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\
|
||||
(:[0-9]*[0-9])?$"
|
||||
event_id:
|
||||
type: string
|
||||
description: Identifies an event.
|
||||
pattern: "^\\$[^:]+:\
|
||||
(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*\
|
||||
([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\
|
||||
(:[0-9]*[0-9])?$"
|
||||
room_id:
|
||||
type: string
|
||||
description: Identifies a room.
|
||||
pattern: "^\\![^:]+:\
|
||||
(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*\
|
||||
([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\
|
||||
(:[0-9]*[0-9])?$"
|
||||
user_id:
|
||||
type: string
|
||||
description: Identifies a user.
|
||||
pattern: "^\\@[^:]+:\
|
||||
(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*\
|
||||
([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\
|
||||
(:[0-9]*[0-9])?$"
|
||||
base64:
|
||||
type: string
|
||||
description: Base64 string without padding.
|
||||
pattern: "^[a-zA-Z0-9/+]*$"
|
||||
hashes:
|
||||
type: object
|
||||
description: Hashes
|
||||
minProperties: 1
|
||||
additionalProperties:
|
||||
$ref: "#/definitions/base64"
|
||||
signatures:
|
||||
type: object
|
||||
description: Signatures
|
||||
additionalProperties:
|
||||
type: object
|
||||
minProperties: 1
|
||||
additionalProperties:
|
||||
$ref: "#/definitions/base64"
|
||||
event_reference:
|
||||
type: array
|
||||
minItems: 2
|
||||
maxItems: 2
|
||||
items:
|
||||
- type: string
|
||||
description: Event id of the referenced event.
|
||||
$ref: "#/definitions/event_id"
|
||||
- type: object
|
||||
description: Reference hashes of the referenced event.
|
||||
$ref: "#/definitions/hashes"
|
||||
event_reference_list:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/definitions/event_reference"
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
|
||||
|
||||
::
|
||||
|
||||
+---------------+
|
||||
| Room |
|
||||
| "Room-ID" |
|
||||
| {State} | +----------------------+
|
||||
| Name------|-------->| Event m.room.name |
|
||||
| Topic | | "Name" |
|
||||
| [Aliases] | +----------------------+ +-------------+
|
||||
| [Members]-|---+ +----------------------+ <----| Start Token |
|
||||
| [Messages] | | | Event m.room.member | +-------------+
|
||||
| | | | +---->| "invite/join/ban" |
|
||||
+---------------+ | "User-ID" |
|
||||
| | +----------------------+
|
||||
| | +----------------------+
|
||||
| | Message | Event m.room.message |
|
||||
| +-------------->| {content} |<--+
|
||||
| +----------------------+ |
|
||||
| Comment +----------------------+ |
|
||||
+------------------>| Event m.room.message | |
|
||||
| {content} | |
|
||||
| "relates-to"-------|---+ +-------------+
|
||||
+----------------------+ <----| End Token |
|
||||
+-------------+
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,317 @@
|
||||
General UI/UX requirements:
|
||||
===========================
|
||||
- Live updates
|
||||
- No flicker:
|
||||
* Sending message (local echo)
|
||||
* Receiving images (encoding w/h)
|
||||
* Scrollback
|
||||
* Resolving display names (from user ID)
|
||||
- Fast startup times
|
||||
- Fast "opening room" times (esp. when clicking through from a notification)
|
||||
- Low latency file transfer.
|
||||
|
||||
Use cases
|
||||
---------
|
||||
- #1: Lightweight IM client (no perm storage) - e.g. Web client
|
||||
- #2: Bug tracking software
|
||||
- #3: Forum
|
||||
- #4: Google + style communities
|
||||
- #5: Email style threading
|
||||
- #6: Multi-column threaded IM
|
||||
- #7: Mobile IM client (perm storage)
|
||||
- #8: MIDI client
|
||||
- #9: Animatrix client
|
||||
- #10: Unity object trees
|
||||
- #11: Social Network ("Walls", PMs, groups)
|
||||
- #12: Minecraft-clone
|
||||
- #13: Global 'Like' widget, which links through to a room.
|
||||
|
||||
|
||||
#1 Web client UI
|
||||
================
|
||||
|
||||
Model::
|
||||
|
||||
Rooms ----< Messages
|
||||
- name - type (call/image)
|
||||
- topic
|
||||
|
||||
Home Screen
|
||||
What's visible:
|
||||
- Recent chats ordered by timestamp of latest event (with # users)
|
||||
- Your own display name, user ID and avatar url
|
||||
- A searchable list of public rooms (with # users and alias + room name + room topic)
|
||||
What you can do:
|
||||
- Create a room (public/private, with alias)
|
||||
- Join a room from alias
|
||||
- Message a user (with user ID)
|
||||
- Leave a recent room
|
||||
- Open a room
|
||||
- Open a chat history link.
|
||||
- Search for a public room.
|
||||
|
||||
Chat Screen
|
||||
What's visible:
|
||||
- Enough scrollback to fill a "screen full" of content.
|
||||
- Each message: timestamp, user ID, display name at the time the message was
|
||||
sent, avatar URL at the time the message was sent, whether it was a bing message
|
||||
or not.
|
||||
- User list: for each user: presence, current avatar url in the room, current
|
||||
display name in the room, power level, ordered by when they were last speaking.
|
||||
- Recents list: (same as Home Screen)
|
||||
- Room name
|
||||
- Room topic
|
||||
- Typing notifications
|
||||
- Desktop/Push Notifications for messages
|
||||
What you can do:
|
||||
- Invite a user
|
||||
- Kick a user
|
||||
- Ban/Unban a user
|
||||
- Leave the room
|
||||
- Send a message (image/text/emote)
|
||||
- Change someone's power level
|
||||
- Change your own display name
|
||||
- Accept an incoming call
|
||||
- Make an outgoing call
|
||||
- Get older messages by scrolling up (scrollback)
|
||||
- Redact a message
|
||||
- Resend a message which was not sent
|
||||
Message sending:
|
||||
- Immediate local echo
|
||||
- Queue up messages which haven't been sent yet
|
||||
- Reordering local echo to where it actually happened
|
||||
VoIP:
|
||||
- One entry in your display for a call (which may contain duration, type, status)
|
||||
- Glare resolution
|
||||
Scrollback:
|
||||
- Display in reverse chronological order by the originating server's timestamp
|
||||
- Terminates at the start of the room (which then makes it impossible to request
|
||||
more scrollback)
|
||||
Local storage:
|
||||
- Driven by desire for fast startup times and minimal network traffic
|
||||
- Display messages from storage and from the network without any gaps in messages.
|
||||
- Persist scrollback if possible: Scrollback from storage first then from the
|
||||
network.
|
||||
Notifications:
|
||||
- Receive notifications for rooms you're interested in (explicitly or from a default)
|
||||
- Maybe per device.
|
||||
- Maybe depending on presence (e.g. idle)
|
||||
- Maybe depending on message volume
|
||||
- Maybe depending on room config options.
|
||||
Message contents:
|
||||
- images
|
||||
- video
|
||||
- rich text
|
||||
- audio
|
||||
- arbitrary files
|
||||
- location
|
||||
- vcards (potentially)
|
||||
|
||||
Chat History Screen
|
||||
What's visible:
|
||||
- The linked message and enough scrollback to fill a "screen full" of content.
|
||||
- Each message: timestamp, user ID, display name at the time the message was
|
||||
sent, avatar URL at the time the message was sent, whether it was a bing message
|
||||
or not.
|
||||
- The historical user list. *TODO: Is this taken at the linked message, or at
|
||||
wherever the user has scrolled to?*
|
||||
What you can do:
|
||||
- Get older messages by scrolling up (scrollback)
|
||||
- Get newer messages by scrolling down
|
||||
|
||||
Public Room Search Screen
|
||||
What's visible:
|
||||
- The current search text.
|
||||
- The homeserver being searched (defaults to the HS the client is connected to).
|
||||
- The results of the current search with enough results to fill the screen
|
||||
with # users and alias + room name + room topic.
|
||||
What you can do:
|
||||
- Change what you are searching for.
|
||||
- Change the server that's being searched.
|
||||
- Scroll down to get more search results.
|
||||
|
||||
User screen
|
||||
What's visible:
|
||||
- Display name
|
||||
- Avatar
|
||||
- User ID
|
||||
What you can do:
|
||||
- Start a chat with the user
|
||||
|
||||
|
||||
#2 Bug tracking UI
|
||||
==================
|
||||
|
||||
Model::
|
||||
|
||||
Projects ----< Issues ---< Comments
|
||||
- key - summary - user
|
||||
- name - ID - message
|
||||
SYN SYN-52 Fix it nooow!
|
||||
|
||||
Landing page
|
||||
What's visible:
|
||||
- Issues assigned to me
|
||||
- Issues I'm watching
|
||||
- Recent activity on other issues (not refined to me)
|
||||
- List of projects
|
||||
What you can do:
|
||||
- View an issue
|
||||
- Create an issue
|
||||
- Sort issues
|
||||
- View a user
|
||||
- View a project
|
||||
- Search for issues (by name, time, priority, description contents, reporter, etc...)
|
||||
|
||||
Issue page
|
||||
What's visible:
|
||||
- Summary of issue
|
||||
- Issue key
|
||||
- Project affected
|
||||
- Description
|
||||
- Comments
|
||||
- Priority, labels, type, purpose, etc..
|
||||
- Reporter/assignee
|
||||
- Creation and last updated times
|
||||
- History of issue changes
|
||||
What you can do:
|
||||
- Comment on issue
|
||||
- Change issue info (labels, type, purpose, etc..)
|
||||
- Open/Close/Resolve the issue
|
||||
- Edit the issue
|
||||
- Watch/Unwatch the issue
|
||||
|
||||
|
||||
#3 Forum UI
|
||||
===========
|
||||
|
||||
Model::
|
||||
|
||||
Forum ----< Boards ----< Threads ----< Messages
|
||||
- Matrix - Dev - HALP! - please halp!
|
||||
|
||||
Main page
|
||||
What's visible:
|
||||
- Categories (containing boards)
|
||||
- Boards (with names and # posts and tagline and latest post)
|
||||
What you can do:
|
||||
- View a board
|
||||
- View the latest message on a board
|
||||
|
||||
Board page
|
||||
What's visible:
|
||||
- Threads (titles, OP, latest post date+author, # replies, # upvotes, whether
|
||||
the OP contains an image or hyperlink (small icon on title))
|
||||
- Whether the thread is answered (with link to the answer)
|
||||
- Pagination for posts within a thread (1,2,3,4,5...10)
|
||||
- Pagination for threads within a board
|
||||
- List of threads in chronological order
|
||||
- Stickied threads
|
||||
What you can do:
|
||||
- View a user
|
||||
- View a thread on a particular page
|
||||
- View the latest message on a thread
|
||||
- View older threads (pagination)
|
||||
- Search the board
|
||||
|
||||
Thread page
|
||||
What's visible:
|
||||
- Messages in chronological order
|
||||
- For each message: author, timestamp, # posts by author, avatar, registration
|
||||
date, status message, message contents, # views of message
|
||||
What you can do:
|
||||
- Upvote the message
|
||||
- Flag the message for a mod
|
||||
- Reply to the message
|
||||
- Subscribe to thread or message's RSS feed
|
||||
- Go to previous/next thread
|
||||
|
||||
|
||||
#4 Google+ community
|
||||
====================
|
||||
|
||||
Model::
|
||||
|
||||
Community -----< Categories ----< Posts ---< Comments
|
||||
Kerbal SP Mods, Help Text Text
|
||||
(no title!)
|
||||
|
||||
Communities page
|
||||
What's visible:
|
||||
- List of communities
|
||||
- For each community: # users, # posts, group pic, title
|
||||
What you can do:
|
||||
- Join a community
|
||||
- View a community
|
||||
|
||||
Community Page
|
||||
What's visible:
|
||||
- Title, pic
|
||||
- List of categories
|
||||
- List of members with avatars (+ total #)
|
||||
- Most recent posts with comments (most recent comment if >1)
|
||||
What you can do:
|
||||
- Join the group
|
||||
- Post a post (with voting and options)
|
||||
- Report abuse
|
||||
- View member
|
||||
- Expand comments
|
||||
- Infinite scrolling
|
||||
- Add a comment to a post
|
||||
- Share a post
|
||||
- +1 a post
|
||||
|
||||
#5 Email style threading
|
||||
========================
|
||||
|
||||
Chat Screen
|
||||
What's visible:
|
||||
- Enough scrollback to fill a "screen full" of content.
|
||||
- Threads:
|
||||
|
||||
- Initially will only display the timestamp and user ID of the *first*
|
||||
message. But can expand to show the entire tree.
|
||||
- Tree of messages indicating which message is a reply to which.
|
||||
- Ordered by the arbitrary field (timestamp of oldest message in thread;
|
||||
newest message in thread; sender id; sender display name; etc)
|
||||
- Each message: timestamp, user ID, display name at the time of the message
|
||||
- Room name
|
||||
- Room topic
|
||||
- Typing notifications
|
||||
- Desktop/Push Notifications for messages
|
||||
What you can do:
|
||||
- Send a message in reply to another message:
|
||||
|
||||
- Immediate local echo, may cause messages to re-order
|
||||
- Messages that haven't reached the server are queued.
|
||||
- Thread is displayed where it should be in the thread order once the
|
||||
message is sent.
|
||||
- Start a new thread by sending a message.
|
||||
|
||||
#6 Multi-threaded IM
|
||||
====================
|
||||
|
||||
Chat Screen
|
||||
What's visible:
|
||||
- A multi-column grid of threads from a number of chatrooms
|
||||
Each concurrent thread is displayed in a different column.
|
||||
The columns start and end as threads split and rejoin the main conversation
|
||||
The messages for each thread are ordered by how recent they are::
|
||||
|
||||
Room #1 Room # 2 Room # 2
|
||||
+------------+ +----------------+ Side thread.
|
||||
| * Message1 | | * Root | +--------------+
|
||||
| * Message2 | | * A1 -> Root | | * B1 -> Root |
|
||||
+------------+ | * A2 -> A1 | | * B2 -> B1 |
|
||||
| * M -> A2, B2 | +--------------+
|
||||
+----------------+
|
||||
|
||||
- Typing notifications. Displayed within the correct thread/column.
|
||||
|
||||
What you can do:
|
||||
- Send a message into a particular thread/column.
|
||||
- Move an *existing* message into a new thread creating a new column
|
||||
- Move an existing message into an existing thread, causing the threads to
|
||||
reconverge (i.e. provide a route from the sidebar back into the existing
|
||||
thread). This does not imply terminating the thread, which can continue
|
||||
independently of the merge.
|
@ -0,0 +1,17 @@
|
||||
.loggedin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing:5px;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
padding:5px;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<div>
|
||||
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8008</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
<div class="loggedin">
|
||||
<form class="createRoomForm">
|
||||
<input type="text" id="roomAlias" placeholder="Room alias (optional)"></input>
|
||||
<input type="button" class="createRoom" value="Create Room"></input>
|
||||
</form>
|
||||
<form class="sendMessageForm">
|
||||
<input type="text" id="roomId" placeholder="Room ID"></input>
|
||||
<input type="text" id="messageBody" placeholder="Message body"></input>
|
||||
<input type="button" class="sendMessage" value="Send Message"></input>
|
||||
</form>
|
||||
<table id="rooms">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Room ID</th>
|
||||
<th>My state</th>
|
||||
<th>Room Alias</th>
|
||||
<th>Latest message</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -0,0 +1,113 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var showLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
getCurrentRoomList();
|
||||
$(".loggedin").css({visibility: "visible"});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getCurrentRoomList = function() {
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
addRoom(rooms[i]);
|
||||
}
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
};
|
||||
|
||||
$('.createRoom').live('click', function() {
|
||||
var roomAlias = $("#roomAlias").val();
|
||||
var data = {};
|
||||
if (roomAlias.length > 0) {
|
||||
data.room_alias_name = roomAlias;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
data.membership = "join"; // you are automatically joined into every room you make.
|
||||
data.latest_message = "";
|
||||
addRoom(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var addRoom = function(data) {
|
||||
row = "<tr>" +
|
||||
"<td>"+data.room_id+"</td>" +
|
||||
"<td>"+data.membership+"</td>" +
|
||||
"<td>"+data.room_alias+"</td>" +
|
||||
"<td>"+data.latest_message+"</td>" +
|
||||
"</tr>";
|
||||
$("#rooms").append(row);
|
||||
};
|
||||
|
||||
$('.sendMessage').live('click', function() {
|
||||
var roomId = $("#roomId").val();
|
||||
var body = $("#messageBody").val();
|
||||
var msgId = $.now();
|
||||
|
||||
if (roomId.length === 0 || body.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8008/_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));
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
body: body
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
$("#messageBody").val("");
|
||||
// wipe the table and reload it. Using the event stream would be the best
|
||||
// solution but that is out of scope of this fiddle.
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
.loggedin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing:5px;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
padding:5px;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<div>
|
||||
<p>This event stream demo requires a home server to be running on http://localhost:8008</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
<div class="loggedin">
|
||||
<form class="sendMessageForm">
|
||||
<input type="button" class="sendMessage" value="Send random message"></input>
|
||||
</form>
|
||||
<p id="streamErrorText"></p>
|
||||
<table id="rooms">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Room ID</th>
|
||||
<th>Latest message</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -0,0 +1,145 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var eventStreamInfo = {
|
||||
from: "END"
|
||||
};
|
||||
|
||||
var roomInfo = [];
|
||||
|
||||
var longpollEventStream = function() {
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$from", eventStreamInfo.from);
|
||||
|
||||
$.getJSON(url, function(data) {
|
||||
eventStreamInfo.from = data.end;
|
||||
|
||||
var hasNewLatestMessage = false;
|
||||
for (var i=0; i<data.chunk.length; ++i) {
|
||||
if (data.chunk[i].type === "m.room.message") {
|
||||
for (var j=0; j<roomInfo.length; ++j) {
|
||||
if (roomInfo[j].room_id === data.chunk[i].room_id) {
|
||||
roomInfo[j].latest_message = data.chunk[i].content.body;
|
||||
hasNewLatestMessage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewLatestMessage) {
|
||||
setRooms(roomInfo);
|
||||
}
|
||||
$("#streamErrorText").text("");
|
||||
longpollEventStream();
|
||||
}).fail(function(err) {
|
||||
$("#streamErrorText").text("Event stream error: "+JSON.stringify($.parseJSON(err.responseText)));
|
||||
setTimeout(longpollEventStream, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
var showLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
longpollEventStream();
|
||||
getCurrentRoomList();
|
||||
$(".loggedin").css({visibility: "visible"});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getCurrentRoomList = function() {
|
||||
$("#roomId").val("");
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
if ("messages" in rooms[i]) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
}
|
||||
}
|
||||
roomInfo = rooms;
|
||||
setRooms(roomInfo);
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
};
|
||||
|
||||
$('.sendMessage').live('click', function() {
|
||||
if (roomInfo.length === 0) {
|
||||
alert("There is no room to send a message to!");
|
||||
return;
|
||||
}
|
||||
|
||||
var index = Math.floor(Math.random() * roomInfo.length);
|
||||
|
||||
sendMessage(roomInfo[index].room_id);
|
||||
});
|
||||
|
||||
var sendMessage = function(roomId) {
|
||||
var body = "jsfiddle message @" + $.now();
|
||||
|
||||
if (roomId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8008/_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));
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
body: body
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
$("#messageBody").val("");
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var setRooms = function(roomList) {
|
||||
// wipe existing entries
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
|
||||
var rows = "";
|
||||
for (var i=0; i<roomList.length; ++i) {
|
||||
row = "<tr>" +
|
||||
"<td>"+roomList[i].room_id+"</td>" +
|
||||
"<td>"+roomList[i].latest_message+"</td>" +
|
||||
"</tr>";
|
||||
rows += row;
|
||||
}
|
||||
|
||||
$("#rooms").append(rows);
|
||||
};
|
||||
|
@ -0,0 +1,43 @@
|
||||
.roomListDashboard, .roomContents, .sendMessageForm {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.roomList {
|
||||
background-color: #909090;
|
||||
}
|
||||
|
||||
.messageWrapper {
|
||||
background-color: #EEEEEE;
|
||||
height: 400px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.membersWrapper {
|
||||
background-color: #EEEEEE;
|
||||
height: 200px;
|
||||
width: 50%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.textEntry {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing:5px;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
.roomList tr:not(:first-child):hover {
|
||||
background-color: orange;
|
||||
cursor: pointer;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
name: Example Matrix Client
|
||||
description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists.
|
||||
authors:
|
||||
- matrix.org
|
||||
resources:
|
||||
- http://matrix.org
|
||||
normalize_css: no
|
@ -0,0 +1,56 @@
|
||||
<div class="signUp">
|
||||
<p>Matrix example application: Requires a local home server running at http://localhost:8008</p>
|
||||
<form class="registrationForm">
|
||||
<p>No account? Register:</p>
|
||||
<input type="text" id="userReg" placeholder="Username"></input>
|
||||
<input type="password" id="passwordReg" placeholder="Password"></input>
|
||||
<input type="button" class="register" value="Register"></input>
|
||||
</form>
|
||||
<form class="loginForm">
|
||||
<p>Got an account? Login:</p>
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="roomListDashboard">
|
||||
<form class="createRoomForm">
|
||||
<input type="text" id="roomAlias" placeholder="Room alias"></input>
|
||||
<input type="button" class="createRoom" value="Create Room"></input>
|
||||
</form>
|
||||
<table id="rooms" class="roomList">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Room</th>
|
||||
<th>My state</th>
|
||||
<th>Latest message</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="roomContents">
|
||||
<p id="roomName">Select a room</p>
|
||||
<div class="messageWrapper">
|
||||
<table id="messages">
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<form class="sendMessageForm">
|
||||
<input type="text" class="textEntry" id="body" placeholder="Enter text here..." onkeydown="javascript:if (event.keyCode == 13) document.getElementById('sendMsg').focus()"></input>
|
||||
<input type="button" class="sendMessage" id="sendMsg" value="Send"></input>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Member list:</p>
|
||||
<div class="membersWrapper">
|
||||
<table id="members">
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,327 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var eventStreamInfo = {
|
||||
from: "END"
|
||||
};
|
||||
|
||||
var roomInfo = [];
|
||||
var memberInfo = [];
|
||||
var viewingRoomId;
|
||||
|
||||
// ************** Event Streaming **************
|
||||
var longpollEventStream = function() {
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$from", eventStreamInfo.from);
|
||||
|
||||
$.getJSON(url, function(data) {
|
||||
eventStreamInfo.from = data.end;
|
||||
|
||||
var hasNewLatestMessage = false;
|
||||
var updatedMemberList = false;
|
||||
var i=0;
|
||||
var j=0;
|
||||
for (i=0; i<data.chunk.length; ++i) {
|
||||
if (data.chunk[i].type === "m.room.message") {
|
||||
console.log("Got new message: " + JSON.stringify(data.chunk[i]));
|
||||
if (viewingRoomId === data.chunk[i].room_id) {
|
||||
addMessage(data.chunk[i]);
|
||||
}
|
||||
|
||||
for (j=0; j<roomInfo.length; ++j) {
|
||||
if (roomInfo[j].room_id === data.chunk[i].room_id) {
|
||||
roomInfo[j].latest_message = data.chunk[i].content.body;
|
||||
hasNewLatestMessage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
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].state_key === data.chunk[i].state_key) {
|
||||
memberInfo[j] = data.chunk[i];
|
||||
updatedMemberList = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!updatedMemberList) {
|
||||
memberInfo.push(data.chunk[i]);
|
||||
updatedMemberList = true;
|
||||
}
|
||||
}
|
||||
if (data.chunk[i].state_key === accountInfo.user_id) {
|
||||
getCurrentRoomList(); // update our join/invite list
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log("Discarding: " + JSON.stringify(data.chunk[i]));
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewLatestMessage) {
|
||||
setRooms(roomInfo);
|
||||
}
|
||||
if (updatedMemberList) {
|
||||
$("#members").empty();
|
||||
for (i=0; i<memberInfo.length; ++i) {
|
||||
addMember(memberInfo[i]);
|
||||
}
|
||||
}
|
||||
longpollEventStream();
|
||||
}).fail(function(err) {
|
||||
setTimeout(longpollEventStream, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
// ************** Registration and Login **************
|
||||
var onLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
longpollEventStream();
|
||||
getCurrentRoomList();
|
||||
$(".roomListDashboard").css({visibility: "visible"});
|
||||
$(".roomContents").css({visibility: "visible"});
|
||||
$(".signUp").css({display: "none"});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
onLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert("Unable to login: is the home server running?");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.register').live('click', function() {
|
||||
var user = $("#userReg").val();
|
||||
var password = $("#passwordReg").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/register",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
onLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
var msg = "Is the home server running?";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson !== null) {
|
||||
msg = errJson.error;
|
||||
}
|
||||
alert("Unable to register: "+msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ************** Creating a room ******************
|
||||
$('.createRoom').live('click', function() {
|
||||
var roomAlias = $("#roomAlias").val();
|
||||
var data = {};
|
||||
if (roomAlias.length > 0) {
|
||||
data.room_alias_name = roomAlias;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
$("#roomAlias").val("");
|
||||
response.membership = "join"; // you are automatically joined into every room you make.
|
||||
response.latest_message = "";
|
||||
|
||||
roomInfo.push(response);
|
||||
setRooms(roomInfo);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ************** Getting current state **************
|
||||
var getCurrentRoomList = function() {
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
if ("messages" in rooms[i]) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
}
|
||||
}
|
||||
roomInfo = rooms;
|
||||
setRooms(roomInfo);
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
};
|
||||
|
||||
var loadRoomContent = function(roomId) {
|
||||
console.log("loadRoomContent " + roomId);
|
||||
viewingRoomId = roomId;
|
||||
$("#roomName").text("Room: "+roomId);
|
||||
$(".sendMessageForm").css({visibility: "visible"});
|
||||
getMessages(roomId);
|
||||
getMemberList(roomId);
|
||||
};
|
||||
|
||||
var getMessages = function(roomId) {
|
||||
$("#messages").empty();
|
||||
var url = "http://localhost:8008/_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]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var getMemberList = function(roomId) {
|
||||
$("#members").empty();
|
||||
memberInfo = [];
|
||||
var url = "http://localhost:8008/_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]);
|
||||
addMember(data.chunk[i]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ************** Sending messages **************
|
||||
$('.sendMessage').live('click', function() {
|
||||
if (viewingRoomId === undefined) {
|
||||
alert("There is no room to send a message to!");
|
||||
return;
|
||||
}
|
||||
var body = $("#body").val();
|
||||
sendMessage(viewingRoomId, body);
|
||||
});
|
||||
|
||||
var sendMessage = function(roomId, body) {
|
||||
var msgId = $.now();
|
||||
|
||||
var url = "http://localhost:8008/_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));
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
body: body
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
$("#body").val("");
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ************** Navigation and DOM manipulation **************
|
||||
var setRooms = function(roomList) {
|
||||
// wipe existing entries
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
|
||||
var rows = "";
|
||||
for (var i=0; i<roomList.length; ++i) {
|
||||
row = "<tr>" +
|
||||
"<td>"+roomList[i].room_id+"</td>" +
|
||||
"<td>"+roomList[i].membership+"</td>" +
|
||||
"<td>"+roomList[i].latest_message+"</td>" +
|
||||
"</tr>";
|
||||
rows += row;
|
||||
}
|
||||
|
||||
$("#rooms").append(rows);
|
||||
|
||||
$('#rooms').find("tr").click(function(){
|
||||
var roomId = $(this).find('td:eq(0)').text();
|
||||
var membership = $(this).find('td:eq(1)').text();
|
||||
if (membership !== "join") {
|
||||
console.log("Joining room " + roomId);
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({membership: "join"}),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
loadRoomContent(roomId);
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
loadRoomContent(roomId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var addMessage = function(data) {
|
||||
|
||||
var msg = data.content.body;
|
||||
if (data.type === "m.room.member") {
|
||||
if (data.content.membership === undefined) {
|
||||
return;
|
||||
}
|
||||
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 if (data.content.membership === "ban") {
|
||||
msg = "<em>was banned from the room</em>";
|
||||
}
|
||||
}
|
||||
if (msg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var row = "<tr>" +
|
||||
"<td>"+data.user_id+"</td>" +
|
||||
"<td>"+msg+"</td>" +
|
||||
"</tr>";
|
||||
$("#messages").append(row);
|
||||
};
|
||||
|
||||
var addMember = function(data) {
|
||||
var row = "<tr>" +
|
||||
"<td>"+data.state_key+"</td>" +
|
||||
"<td>"+data.content.membership+"</td>" +
|
||||
"</tr>";
|
||||
$("#members").append(row);
|
||||
};
|
||||
|
@ -0,0 +1,7 @@
|
||||
.loggedin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<div>
|
||||
<p>This registration/login demo requires a home server to be running on http://localhost:8008</p>
|
||||
</div>
|
||||
<form class="registrationForm">
|
||||
<input type="text" id="user" placeholder="Username"></input>
|
||||
<input type="password" id="password" placeholder="Password"></input>
|
||||
<input type="button" class="register" value="Register"></input>
|
||||
</form>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
<div class="loggedin">
|
||||
<p id="welcomeText"></p>
|
||||
<input type="button" class="testToken" value="Test token"></input>
|
||||
<input type="button" class="logout" value="Logout"></input>
|
||||
<p id="imSyncText"></p>
|
||||
</div>
|
||||
|
@ -0,0 +1,79 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var showLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
$(".loggedin").css({visibility: "visible"});
|
||||
$("#welcomeText").text("Welcome " + accountInfo.user_id+". Your access token is: " +
|
||||
accountInfo.access_token);
|
||||
};
|
||||
|
||||
$('.register').live('click', function() {
|
||||
var user = $("#user").val();
|
||||
var password = $("#password").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/register",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var login = function(user, password) {
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.getJSON("http://localhost:8008/_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;
|
||||
}
|
||||
login(user, password);
|
||||
});
|
||||
});
|
||||
|
||||
$('.logout').live('click', function() {
|
||||
accountInfo = {};
|
||||
$("#imSyncText").text("");
|
||||
$(".loggedin").css({visibility: "hidden"});
|
||||
});
|
||||
|
||||
$('.testToken').live('click', function() {
|
||||
var url = "http://localhost:8008/_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) {
|
||||
$("#imSyncText").text(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
.loggedin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing:5px;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
padding:5px;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<div>
|
||||
<p>This room membership demo requires a home server to be running on http://localhost:8008</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
<div class="loggedin">
|
||||
<form class="createRoomForm">
|
||||
<input type="button" class="createRoom" value="Create Room"></input>
|
||||
</form>
|
||||
<form class="changeMembershipForm">
|
||||
<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>
|
||||
</select>
|
||||
<input type="button" class="changeMembership" value="Change Membership"></input>
|
||||
</form>
|
||||
<form class="joinAliasForm">
|
||||
<input type="text" id="roomAlias" placeholder="Room Alias (#name:domain)"></input>
|
||||
<input type="button" class="joinAlias" value="Join via Alias"></input>
|
||||
</form>
|
||||
<table id="rooms">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Room ID</th>
|
||||
<th>My state</th>
|
||||
<th>Room Alias</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -0,0 +1,141 @@
|
||||
var accountInfo = {};
|
||||
|
||||
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:8008/_matrix/client/api/v1/login",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
var errMsg = "To try this, you need a home server running!";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson) {
|
||||
errMsg = JSON.stringify(errJson);
|
||||
}
|
||||
alert(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getCurrentRoomList = function() {
|
||||
$("#roomId").val("");
|
||||
// wipe the table and reload it. Using the event stream would be the best
|
||||
// solution but that is out of scope of this fiddle.
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
addRoom(rooms[i]);
|
||||
}
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
};
|
||||
|
||||
$('.createRoom').live('click', function() {
|
||||
var data = {};
|
||||
$.ajax({
|
||||
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
data.membership = "join"; // you are automatically joined into every room you make.
|
||||
data.latest_message = "";
|
||||
addRoom(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var addRoom = function(data) {
|
||||
row = "<tr>" +
|
||||
"<td>"+data.room_id+"</td>" +
|
||||
"<td>"+data.membership+"</td>" +
|
||||
"<td>"+data.room_alias+"</td>" +
|
||||
"</tr>";
|
||||
$("#rooms").append(row);
|
||||
};
|
||||
|
||||
$('.changeMembership').live('click', function() {
|
||||
var roomId = $("#roomId").val();
|
||||
var member = $("#targetUser").val();
|
||||
var membership = $("#membership").val();
|
||||
|
||||
if (roomId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
url = url.replace("$membership", membership);
|
||||
|
||||
var data = {};
|
||||
|
||||
if (membership === "invite") {
|
||||
data = {
|
||||
user_id: member
|
||||
};
|
||||
}
|
||||
|
||||
$.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:8008/_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: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({}),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue