Browse Source

Merge pull request #48 from longsleep/lookup-support

Contact support
pull/49/head
Simon Eisenmann 12 years ago
parent
commit
3270291f4e
  1. 162
      doc/CHANNELING-API.txt
  2. 4
      html/main.html
  3. 5
      server.conf.in
  4. 68
      src/app/spreed-webrtc-server/channeling.go
  5. 29
      src/app/spreed-webrtc-server/contact.go
  6. 218
      src/app/spreed-webrtc-server/hub.go
  7. 7
      src/app/spreed-webrtc-server/main.go
  8. 45
      src/app/spreed-webrtc-server/server.go
  9. 87
      src/app/spreed-webrtc-server/session.go
  10. 112
      src/app/spreed-webrtc-server/user.go
  11. 5
      src/app/spreed-webrtc-server/users.go
  12. 62
      src/styles/components/_buddylist.scss
  13. 2
      src/styles/components/_chat.scss
  14. 1
      src/styles/global/_base.scss
  15. 49
      static/js/controllers/chatroomcontroller.js
  16. 6
      static/js/controllers/controllers.js
  17. 38
      static/js/controllers/mediastreamcontroller.js
  18. 66
      static/js/controllers/usersettingscontroller.js
  19. 26
      static/js/directives/audiolevel.js
  20. 8
      static/js/directives/audiovideo.js
  21. 49
      static/js/directives/buddylist.js
  22. 103
      static/js/directives/chat.js
  23. 72
      static/js/directives/contactrequest.js
  24. 6
      static/js/directives/directives.js
  25. 38
      static/js/directives/settings.js
  26. 2
      static/js/directives/usability.js
  27. 14
      static/js/filters/buddyimagesrc.js
  28. 4
      static/js/filters/displayname.js
  29. 39
      static/js/mediastream/api.js
  30. 51
      static/js/services/animationframe.js
  31. 12
      static/js/services/appdata.js
  32. 33
      static/js/services/buddydata.js
  33. 464
      static/js/services/buddylist.js
  34. 191
      static/js/services/buddysession.js
  35. 103
      static/js/services/contactdata.js
  36. 265
      static/js/services/contacts.js
  37. 73
      static/js/services/localstorage.js
  38. 2
      static/js/services/mediastream.js
  39. 21
      static/js/services/services.js
  40. 6
      static/partials/buddy.html
  41. 13
      static/partials/buddyactions.html
  42. 2
      static/partials/buddylist.html
  43. 10
      static/partials/contactrequest.html
  44. 2
      static/partials/page/welcome.html
  45. 115
      static/partials/settings.html

162
doc/CHANNELING-API.txt

@ -52,11 +52,14 @@ Sending vs receiving document data encapsulation @@ -52,11 +52,14 @@ Sending vs receiving document data encapsulation
{
"Type": "Whatever",
"Whatever": { /* your document */ }
"Whatever": { /* your document */ },
"Iid": "request-identifier-unique-to-client"
}
So any document you sent, you have to specify a Type key pointing
to the key where the real document is to be found.
to the key where the real document is to be found. The Iid field is
optional and is returned back with the response wrapper document to
match requests with response data when supported by the type.
Received documents are wrapped by a special Document which provides
additional information.
@ -64,7 +67,8 @@ Sending vs receiving document data encapsulation @@ -64,7 +67,8 @@ Sending vs receiving document data encapsulation
{
"From": "4",
"To": "5",
"Data": {}
"Data": {},
"Iid": "request-identifier-unique-to-client"
}
The Data key contains the real Document.
@ -76,6 +80,9 @@ Sending vs receiving document data encapsulation @@ -76,6 +80,9 @@ Sending vs receiving document data encapsulation
To : The Id, the server send this Document to. Should be the same as
your current Self Id.
Data : Contains the payload.
Iid : Optional request identifier to match this response to the calling
request. Only available when sent by the client and the requested
type implementation does support it.
Special purpose documents for channling
@ -87,6 +94,7 @@ Special purpose documents for channling @@ -87,6 +94,7 @@ Special purpose documents for channling
"Id": "4",
"Sid": "5157",
"Userid": "",
"Suserid": "",
"Token": "some-very-long-string",
"Version": "server-version-number",
"Turn": {
@ -108,20 +116,21 @@ Special purpose documents for channling @@ -108,20 +116,21 @@ Special purpose documents for channling
Keys:
Type : Self (string)
Id : Public Session id for this connection (string).
Sid : Secure (non public) id for this session (string).
Userid : User id if this session belongs to an authenticated user. Else empty.
Token : Security token (string), to restablish connection with the same
Type : Self (string)
Id : Public Session id for this connection (string).
Sid : Secure (non public) id for this session (string).
Userid : User id if this session belongs to an authenticated user. Else empty.
Suserid : Secure (non public) user id if session has an user id. Else empty.
Token : Security token (string), to restablish connection with the same
session. Pass the value as URL query parameter t, to the websocket URL.
Version: Server version number. Use this to detect server upgrades.
Turn : Mapping (interface{}) to contain TURN server details, like
urls, password and username. See
http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
and TURN REST API section in
https://code.google.com/p/rfc5766-turn-server/wiki/turnserver
for details.
Stun : Array with STUN server URLs.
Version : Server version number. Use this to detect server upgrades.
Turn : Mapping (interface{}) to contain TURN server details, like
urls, password and username. See
http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
and TURN REST API section in
https://code.google.com/p/rfc5766-turn-server/wiki/turnserver
for details.
Stun : Array with STUN server URLs.
You can also send an empty Self document to the server to make the server
transmit a fresh Self document (eg. to refresh when ttl was reached). Please
@ -273,7 +282,8 @@ Additional types for session listing and notifications @@ -273,7 +282,8 @@ Additional types for session listing and notifications
"Id": "7",
"Userid": "u7",
"Ua": "Chrome 28",
"Status": null
"Status": null,
"Prio": 100
}
Note: The Userid field is only present if that session belongs to a known user.
@ -303,11 +313,7 @@ Additional types for session listing and notifications @@ -303,11 +313,7 @@ Additional types for session listing and notifications
Users (Request uses empty data)
{
"Type": "Users",
"Users": {
"Type": "Users",
"Users": {}
}
"Type": "Users"
}
Users (Response with data)
@ -319,25 +325,26 @@ Additional types for session listing and notifications @@ -319,25 +325,26 @@ Additional types for session listing and notifications
"Type": "Online",
"Id": "1",
"Ua": "Firefox 27",
"Status": {...}
"Status": {...},
"Prio": 100
},
{
"Type": "Online",
"Id": "3",
"Userid": "u3",
"Ua": "Chrome 28",
"Status": {...}
"Status": {...},
"Prio": 100
},
{
"Type": "Online",
"Id": "4",
"Userid": "u4",
"Ua": "Chrome 28",
"Status": {...}
"Status": {...},
"Prio": 100
}
],
"Index": 0,
"Batch": 0
]
}
Note: The Userid field is only present, if that session belongs to a known user.
@ -385,6 +392,47 @@ User authorization and session authentication @@ -385,6 +392,47 @@ User authorization and session authentication
the session (disconnect) and forget the token.
Information retrieval
Sessions (Request uses Id, Token and Type)
{
"Type": "Sessions",
"Sessions": {
"Type": "Token type",
"Token": "Request token"
}
}
Valid known token types are: "contact".
Sessions (Response with Id, Token and Type from request and
populated Session list).
{
"Type": "Sessions",
"Sessions": {
"Type": "Type as in request",
"Token": "Token as in request"
},
"Users": [
{
"Type": "Online",
"Id": "1",
"Ua": "Firefox 27",
"Status": {...}
},
{
"Type": "Online",
"Id": "3",
"Userid": "u3",
"Ua": "Chrome 28",
"Status": {...}
}, ...
]
}
Chat messages and status information
The chat is used to transfer simple messages ore more complex structures
@ -457,6 +505,48 @@ Chat messages and status information @@ -457,6 +505,48 @@ Chat messages and status information
message is shown or not. For file transfer information the message is
always "File".
Chat with contact request/confirm information
Request to create a contact token with Id.
{
"Message": "Some message",
"Time": "2013-11-20T16:28:42+01:00",
"Status": {
"ContactRequest": {
"Id": "client-generated-id"
}
}
}
Reply with success (Sever generates and inserts token).
{
"Message": "Some response message",
"Time": "2013-11-20T16:28:59+01:00",
"Status": {
"ContactRequest": {
"Id": "request-id",
"Success": true,
"Token": "server-generated-token-on-success"
}
}
}
Or reject (no reply is also possible).
{
"Message": "Some response message",
"Time": "2013-11-20T16:28:59+01:00",
"Status": {
"ContactRequest": {
"Id": "request-id",
"Success": false
}
}
}
Chat message deliver status extensions
Send chat messages as normal, but add the "Mid" field which is a
@ -536,22 +626,24 @@ Chat messages and status information @@ -536,22 +626,24 @@ Chat messages and status information
}
Request being called from audio mixer
Request an automatic callback, by sending a chat message with the AutoCall
document in Status.
{
"Type": "Chat",
"Chat": {
"Message": null,
"Message": "Call me back",
"Status": {
"type": "conference",
"id": "my-conference-room"
"AutoCall": {
"Type": "conference",
"Id": "my-conference-room"
}
}
}
This can be sent to any participant that has a "Status" containing the "isMixer"
flag with a "true" value. The peer will then try to establish a peer connection
to the caller which it needs to pick up to join an audio conference.
For example this can be sent to sessions which have "autoCalls" set in session
status. The peer will then try to establish a peer connection to the caller which
the client eeds to pick up automatically.
Data channel only messages

4
html/main.html

@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
<status-message></status-message>
<div class="ng-cloak right">
<button title="{{_('Share your screen')}}" class="btn aenablebtn" ng-show="isChrome && webrtcDetectedVersion >= 32 && (status=='connected' || status=='conference' || layout.screenshare)" ng-model="layout.screenshare" btn-checkbox><i class="fa fa-desktop"></i></button>
<button title="{{_('Chat')}}" ng-show="roomstatus" class="btn" ng-class="{messagesunseen: chatMessagesUnseen>0}" ng-model="layout.chat" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-comments-o"></i><span class="badge" ng-show="chatMessagesUnseen">{{chatMessagesUnseen}}</span></button>
<button title="{{_('Chat')}}" class="btn" ng-class="{messagesunseen: chatMessagesUnseen>0}" ng-model="layout.chat" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-comments-o"></i><span class="badge" ng-show="chatMessagesUnseen">{{chatMessagesUnseen}}</span></button>
<button title="{{_('Mute microphone')}}" class="btn amutebtn" ng-model="microphoneMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-microphone-slash"></i></button>
<button title="{{_('Turn camera off')}}" class="btn amutebtn" ng-model="cameraMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-eye-slash"></i></button>
<button title="{{_('Settings')}}" class="btn" ng-model="layout.settings" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-cog"></i></button>
@ -35,7 +35,7 @@ @@ -35,7 +35,7 @@
<screenshare/>
</div>
<div class="ng-cloak nicescroll" id="rightslide">
<div class="rightslidepane" ng-show="roomstatus">
<div class="rightslidepane">
<div id="buddylist"><buddy-list/></div>
<div id="chat"><chat/></div>
</div>

5
server.conf.in

@ -60,6 +60,11 @@ listen = 127.0.0.1:8080 @@ -60,6 +60,11 @@ listen = 127.0.0.1:8080
; Session secret to use for session id generator. 32 or 64 bytes of random data
; are recommented.
sessionSecret = the-default-secret-do-not-keep-me
; Encryption secret protecting the data in generated server side tokens. Use
; 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. When you change
; the encryption secret, stored authentications, sessions and contacts become
; invalid.
encryptionSecret = tne-default-encryption-block-key
; Full path to a text file containig client tokens which a user needs to enter
; when accessing the web client. Each line in this file represents a valid
; token.

68
src/app/spreed-webrtc-server/channeling.go

@ -50,6 +50,7 @@ type DataSelf struct { @@ -50,6 +50,7 @@ type DataSelf struct {
Id string
Sid string
Userid string
Suserid string
Token string
Version string
Turn *DataTurn
@ -66,12 +67,19 @@ type DataTurn struct { @@ -66,12 +67,19 @@ type DataTurn struct {
type DataSession struct {
Type string
Id string
Userid string `json:"Userid,omitempty"`
Ua string `json:"Ua,omitempty"`
Token string `json:"Token,omitempty"`
Version string `json:"Version,omitempty"`
Rev uint64 `json:"Rev,omitempty"`
Userid string `json:",omitempty"`
Ua string `json:",omitempty"`
Token string `json:",omitempty"`
Version string `json:",omitempty"`
Rev uint64 `json:",omitempty"`
Prio int `json:",omitempty"`
Status interface{}
stamp int64
}
type DataUser struct {
Id string
Sessions int
}
type DataBye struct {
@ -92,16 +100,41 @@ type DataChat struct { @@ -92,16 +100,41 @@ type DataChat struct {
}
type DataChatMessage struct {
Mid string
Message string
Time string
NoEcho bool `json:"NoEcho,omitempty"`
Status interface{}
NoEcho bool `json:",omitempty"`
Mid string `json:",omitempty"`
Status *DataChatStatus
}
type DataChatMessageStatus struct {
State string
Mid string
type DataChatStatus struct {
Typing string `json:",omitempty"`
State string `json:",omitempty"`
Mid string `json:",omitempty"`
SeenMids []string `json:",omitempty"`
FileInfo *DataFileInfo `json:",omitempty"`
ContactRequest *DataContactRequest `json:",omitempty"`
AutoCall *DataAutoCall `json:",omitempty"`
}
type DataFileInfo struct {
Id string `json:"id"`
Chunks uint64 `json:"chunks"`
Name string `json:"name"`
Size uint64 `json:"size"`
Type string `json:"type"`
}
type DataContactRequest struct {
Id string
Success bool
Userid string `json:",omitempty"`
Token string `json:",omitempty"`
}
type DataAutoCall struct {
Id string
Type string
}
type DataIncoming struct {
@ -116,19 +149,26 @@ type DataIncoming struct { @@ -116,19 +149,26 @@ type DataIncoming struct {
Conference *DataConference
Alive *DataAlive
Authentication *DataAuthentication
Sessions *DataSessions
Iid string `json:",omitempty"`
}
type DataOutgoing struct {
Data interface{}
From string
To string
Iid string `json:",omitempty"`
}
type DataSessions struct {
Type string
Sessions *DataSessionsRequest `json:",omitempty"`
Users []*DataSession
}
type DataSessionsRequest struct {
Token string
Type string
Users []*DataSession
Index uint64
Batch uint64
}
type DataConference struct {

29
src/app/spreed-webrtc-server/contact.go

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import ()
type Contact struct {
A string
B string
}

218
src/app/spreed-webrtc-server/hub.go

@ -23,6 +23,7 @@ package main @@ -23,6 +23,7 @@ package main
import (
"bytes"
"crypto/aes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
@ -56,11 +57,13 @@ type HubStat struct { @@ -56,11 +57,13 @@ type HubStat struct {
Rooms int `json:"rooms"`
Connections int `json:"connections"`
Sessions int `json:"sessions"`
Users int `json:"users"`
Count uint64 `json:"count"`
BroadcastChatMessages uint64 `json:"broadcastchatmessages"`
UnicastChatMessages uint64 `json:"unicastchatmessages"`
IdsInRoom map[string][]string `json:"idsinroom,omitempty"`
SessionsById map[string]*DataSession `json:"sessionsbyid,omitempty"`
UsersById map[string]*DataUser `json:"usersbyid,omitempty"`
ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"`
}
@ -69,9 +72,11 @@ type Hub struct { @@ -69,9 +72,11 @@ type Hub struct {
connectionTable map[string]*Connection
sessionTable map[string]*Session
roomTable map[string]*RoomWorker
userTable map[string]*User
version string
config *Config
sessionSecret []byte
encryptionSecret []byte
turnSecret []byte
tickets *securecookie.SecureCookie
count uint64
@ -83,29 +88,39 @@ type Hub struct { @@ -83,29 +88,39 @@ type Hub struct {
realm string
tokenName string
useridRetriever func(*http.Request) (string, error)
contacts *securecookie.SecureCookie
}
func NewHub(version string, config *Config, sessionSecret, turnSecret, realm string) *Hub {
func NewHub(version string, config *Config, sessionSecret, encryptionSecret, turnSecret, realm string) *Hub {
h := &Hub{
connectionTable: make(map[string]*Connection),
sessionTable: make(map[string]*Session),
roomTable: make(map[string]*RoomWorker),
version: version,
config: config,
sessionSecret: []byte(sessionSecret),
turnSecret: []byte(turnSecret),
realm: realm,
connectionTable: make(map[string]*Connection),
sessionTable: make(map[string]*Session),
roomTable: make(map[string]*RoomWorker),
userTable: make(map[string]*User),
version: version,
config: config,
sessionSecret: []byte(sessionSecret),
encryptionSecret: []byte(encryptionSecret),
turnSecret: []byte(turnSecret),
realm: realm,
}
if len(h.sessionSecret) < 32 {
log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(h.sessionSecret))
}
h.tickets = securecookie.New(h.sessionSecret, nil)
h.tickets = securecookie.New(h.sessionSecret, h.encryptionSecret)
h.tickets.MaxAge(86400 * 30) // 30 days
h.tickets.HashFunc(sha256.New)
h.tickets.BlockFunc(aes.NewCipher)
h.buffers = NewBufferCache(1024, bytes.MinRead)
h.buddyImages = NewImageCache()
h.tokenName = fmt.Sprintf("token@%s", h.realm)
h.contacts = securecookie.New(h.sessionSecret, h.encryptionSecret)
h.contacts.MaxAge(0)
h.contacts.HashFunc(sha256.New)
h.contacts.BlockFunc(aes.NewCipher)
return h
}
@ -117,6 +132,7 @@ func (h *Hub) Stat(details bool) *HubStat { @@ -117,6 +132,7 @@ func (h *Hub) Stat(details bool) *HubStat {
Rooms: len(h.roomTable),
Connections: len(h.connectionTable),
Sessions: len(h.sessionTable),
Users: len(h.userTable),
Count: h.count,
BroadcastChatMessages: atomic.LoadUint64(&h.broadcastChatMessages),
UnicastChatMessages: atomic.LoadUint64(&h.unicastChatMessages),
@ -136,6 +152,11 @@ func (h *Hub) Stat(details bool) *HubStat { @@ -136,6 +152,11 @@ func (h *Hub) Stat(details bool) *HubStat {
sessions[sessionid] = session.Data()
}
stat.SessionsById = sessions
users := make(map[string]*DataUser)
for userid, user := range h.userTable {
users[userid] = user.Data()
}
stat.UsersById = users
connections := make(map[string]string)
for id, connection := range h.connectionTable {
connections[fmt.Sprintf("%d", connection.Idx)] = id
@ -166,6 +187,16 @@ func (h *Hub) CreateTurnData(id string) *DataTurn { @@ -166,6 +187,16 @@ func (h *Hub) CreateTurnData(id string) *DataTurn {
}
func (h *Hub) CreateSuserid(session *Session) (suserid string) {
userid := session.Userid()
if userid != "" {
m := hmac.New(sha256.New, h.encryptionSecret)
m.Write([]byte(userid))
suserid = base64.StdEncoding.EncodeToString(m.Sum(nil))
}
return
}
func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session {
// NOTE(longsleep): Is it required to make this a secure cookie,
@ -183,8 +214,8 @@ func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session { @@ -183,8 +214,8 @@ func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session {
if st == nil {
sid := NewRandomString(32)
id, _ := h.tickets.Encode("id", sid)
session = NewSession(id, sid, userid)
log.Println("Created new session id", len(id), id, sid, userid)
session = NewSession(id, sid)
log.Println("Created new session id", len(id), id, sid)
} else {
if userid == "" {
userid = st.Userid
@ -192,7 +223,11 @@ func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session { @@ -192,7 +223,11 @@ func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session {
if !usersEnabled {
userid = ""
}
session = NewSession(st.Id, st.Sid, userid)
session = NewSession(st.Id, st.Sid)
}
if userid != "" {
h.authenticateHandler(session, st, userid)
}
return session
@ -337,8 +372,18 @@ func (h *Hub) unregisterHandler(c *Connection) { @@ -337,8 +372,18 @@ func (h *Hub) unregisterHandler(c *Connection) {
return
}
session := c.Session
suserid := session.Userid()
delete(h.connectionTable, c.Id)
delete(h.sessionTable, c.Id)
if session != nil && suserid != "" {
user, ok := h.userTable[suserid]
if ok {
empty := user.RemoveSession(session)
if empty {
delete(h.userTable, suserid)
}
}
}
h.mutex.Unlock()
if session != nil {
h.buddyImages.Delete(session.Id)
@ -362,11 +407,11 @@ func (h *Hub) unicastHandler(m *MessageRequest) { @@ -362,11 +407,11 @@ func (h *Hub) unicastHandler(m *MessageRequest) {
}
func (h *Hub) aliveHandler(c *Connection, alive *DataAlive) {
func (h *Hub) aliveHandler(c *Connection, alive *DataAlive, iid string) {
aliveJson := h.buffers.New()
encoder := json.NewEncoder(aliveJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: alive})
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: alive, Iid: iid})
if err != nil {
log.Println("Alive error while encoding JSON", err)
aliveJson.Decref()
@ -377,6 +422,59 @@ func (h *Hub) aliveHandler(c *Connection, alive *DataAlive) { @@ -377,6 +422,59 @@ func (h *Hub) aliveHandler(c *Connection, alive *DataAlive) {
}
func (h *Hub) sessionsHandler(c *Connection, srq *DataSessionsRequest, iid string) {
var users []*DataSession
switch srq.Type {
case "contact":
contact := &Contact{}
err := h.contacts.Decode("contact", srq.Token, contact)
if err != nil {
log.Println("Failed to decode incoming contact token", err, srq.Token)
return
}
// Use the userid which is not ours from the contact data.
var userid string
suserid := c.Session.Userid()
if contact.A == suserid {
userid = contact.B
} else if contact.B == suserid {
userid = contact.A
}
if userid == "" {
log.Println("Ignoring foreign contact token", contact.A, contact.B)
return
}
// Find foreign user.
h.mutex.RLock()
defer h.mutex.RUnlock()
user, ok := h.userTable[userid]
if !ok {
return
}
// Add sessions for forein user.
users = user.SessionsData()
default:
log.Println("Unkown incoming sessions request type", srq.Type)
}
if users != nil {
sessions := &DataSessions{Type: "Sessions", Users: users, Sessions: srq}
sessionsJson := h.buffers.New()
encoder := json.NewEncoder(sessionsJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: sessions, Iid: iid})
if err != nil {
log.Println("Sessions error while encoding JSON", err)
sessionsJson.Decref()
return
}
c.send(sessionsJson)
sessionsJson.Decref()
}
}
func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
//fmt.Println("Userupdate", u)
@ -385,7 +483,6 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 { @@ -385,7 +483,6 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
h.mutex.RUnlock()
var rev uint64
if ok {
rev = session.Update(s)
if s.Status != nil {
status, ok := s.Status.(map[string]interface{})
if ok && status["buddyPicture"] != nil {
@ -398,6 +495,7 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 { @@ -398,6 +495,7 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
}
}
}
rev = session.Update(s)
} else {
log.Printf("Update data for unknown user %s\n", s.Id)
}
@ -423,3 +521,91 @@ func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) { @@ -423,3 +521,91 @@ func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) {
return nonce, nil
}
func (h *Hub) authenticateHandler(session *Session, st *SessionToken, userid string) error {
err := session.Authenticate(h.realm, st, userid)
if err == nil {
// Authentication success.
suserid := session.Userid()
h.mutex.Lock()
user, ok := h.userTable[suserid]
if !ok {
user = NewUser(suserid)
h.userTable[suserid] = user
}
h.mutex.Unlock()
user.AddSession(session)
}
return err
}
func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactRequest) error {
var err error
if cr.Success {
// Client replied with success.
// Decode Token and make sure c.Session.Userid and the to Session.Userid are a match.
contact := &Contact{}
err = h.contacts.Decode("contact", cr.Token, contact)
if err != nil {
return err
}
suserid := c.Session.Userid()
if suserid == "" {
return errors.New("no userid")
}
h.mutex.RLock()
session, ok := h.sessionTable[to]
h.mutex.RUnlock()
if !ok {
return errors.New("unknown to session for confirm")
}
userid := session.Userid()
if userid == "" {
return errors.New("to has no userid for confirm")
}
if suserid != contact.A {
return errors.New("contact mismatch in a")
}
if userid != contact.B {
return errors.New("contact mismatch in b")
}
} else {
if cr.Token != "" {
// Client replied with no success.
// Remove token.
cr.Token = ""
} else {
// New request.
// Create Token with flag and c.Session.Userid and the to Session.Userid.
suserid := c.Session.Userid()
if suserid == "" {
return errors.New("no userid")
}
h.mutex.RLock()
session, ok := h.sessionTable[to]
h.mutex.RUnlock()
if !ok {
return errors.New("unknown to session")
}
userid := session.Userid()
if userid == "" {
return errors.New("to has no userid")
}
if userid == suserid {
return errors.New("to userid cannot be the same as own userid")
}
// Create object.
contact := &Contact{userid, suserid}
// Serialize.
cr.Token, err = h.contacts.Encode("contact", contact)
}
}
return err
}

7
src/app/spreed-webrtc-server/main.go

@ -211,6 +211,11 @@ func runner(runtime phoenix.Runtime) error { @@ -211,6 +211,11 @@ func runner(runtime phoenix.Runtime) error {
return fmt.Errorf("No sessionSecret in config file.")
}
encryptionSecret, err := runtime.GetString("app", "encryptionSecret")
if err != nil {
return fmt.Errorf("No encryptionSecret in config file.")
}
tokenFile, err := runtime.GetString("app", "tokenFile")
if err == nil {
if !httputils.HasFilePath(path.Clean(tokenFile)) {
@ -340,7 +345,7 @@ func runner(runtime phoenix.Runtime) error { @@ -340,7 +345,7 @@ func runner(runtime phoenix.Runtime) error {
computedRealm := fmt.Sprintf("%s.%s", serverRealm, serverToken)
// Create our hub instance.
hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret, computedRealm)
hub := NewHub(runtimeVersion, config, sessionSecret, encryptionSecret, turnSecret, computedRealm)
// Set number of go routines if it is 1
if goruntime.GOMAXPROCS(0) == 1 {

45
src/app/spreed-webrtc-server/server.go

@ -44,7 +44,8 @@ func (s *Server) OnRegister(c *Connection) { @@ -44,7 +44,8 @@ func (s *Server) OnRegister(c *Connection) {
Type: "Self",
Id: c.Id,
Sid: c.Session.Sid,
Userid: c.Session.Userid,
Userid: c.Session.Userid(),
Suserid: c.h.CreateSuserid(c.Session),
Token: token,
Version: c.h.version,
Turn: c.h.CreateTurnData(c.Id),
@ -67,7 +68,7 @@ func (s *Server) OnUnregister(c *Connection) { @@ -67,7 +68,7 @@ func (s *Server) OnUnregister(c *Connection) {
func (s *Server) OnText(c *Connection, b Buffer) {
//log.Printf("OnText from %d: %s\n", c.id, b)
//log.Printf("OnText from %d: %s\n", c.Id, b)
var msg DataIncoming
err := json.Unmarshal(b.Bytes(), &msg)
if err != nil {
@ -106,7 +107,7 @@ func (s *Server) OnText(c *Connection, b Buffer) { @@ -106,7 +107,7 @@ func (s *Server) OnText(c *Connection, b Buffer) {
// TODO(longsleep): Validate Answer
s.Unicast(c, msg.Answer.To, msg.Answer)
case "Users":
if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
if c.Hello {
s.Users(c)
}
case "Authentication":
@ -121,7 +122,7 @@ func (s *Server) OnText(c *Connection, b Buffer) { @@ -121,7 +122,7 @@ func (s *Server) OnText(c *Connection, b Buffer) {
case "Status":
//log.Println("Status", msg.Status)
s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status})
if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
if c.Hello {
s.Broadcast(c, c.Session.DataSessionStatus())
}
case "Chat":
@ -132,16 +133,24 @@ func (s *Server) OnText(c *Connection, b Buffer) { @@ -132,16 +133,24 @@ func (s *Server) OnText(c *Connection, b Buffer) {
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339)
if msg.Chat.To == "" {
// TODO(longsleep): Check if chat broadcast is allowed.
if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
if c.Hello {
atomic.AddUint64(&c.h.broadcastChatMessages, 1)
s.Broadcast(c, msg.Chat)
}
} else {
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil {
err = s.ContactRequest(c, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest)
if err != nil {
log.Println("Ignoring invalid contact request.", err)
return
}
msg.Chat.Chat.Status.ContactRequest.Userid = c.Session.Userid()
}
atomic.AddUint64(&c.h.unicastChatMessages, 1)
s.Unicast(c, msg.Chat.To, msg.Chat)
if msg.Chat.Chat.Mid != "" {
// Send out delivery confirmation status chat message.
s.Unicast(c, c.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatMessageStatus{State: "sent"}}})
s.Unicast(c, c.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}})
}
}
case "Conference":
@ -158,7 +167,9 @@ func (s *Server) OnText(c *Connection, b Buffer) { @@ -158,7 +167,9 @@ func (s *Server) OnText(c *Connection, b Buffer) {
}
}
case "Alive":
s.Alive(c, msg.Alive)
s.Alive(c, msg.Alive, msg.Iid)
case "Sessions":
s.Sessions(c, msg.Sessions.Sessions, msg.Iid)
default:
log.Println("OnText unhandled message type", msg.Type)
}
@ -181,9 +192,15 @@ func (s *Server) Unicast(c *Connection, to string, m interface{}) { @@ -181,9 +192,15 @@ func (s *Server) Unicast(c *Connection, to string, m interface{}) {
b.Decref()
}
func (s *Server) Alive(c *Connection, alive *DataAlive) {
func (s *Server) Alive(c *Connection, alive *DataAlive, iid string) {
c.h.aliveHandler(c, alive)
c.h.aliveHandler(c, alive, iid)
}
func (s *Server) Sessions(c *Connection, srq *DataSessionsRequest, iid string) {
c.h.sessionsHandler(c, srq, iid)
}
@ -194,6 +211,12 @@ func (s *Server) UpdateSession(c *Connection, su *SessionUpdate) uint64 { @@ -194,6 +211,12 @@ func (s *Server) UpdateSession(c *Connection, su *SessionUpdate) uint64 {
}
func (s *Server) ContactRequest(c *Connection, to string, cr *DataContactRequest) (err error) {
return c.h.contactrequestHandler(c, to, cr)
}
func (s *Server) Broadcast(c *Connection, m interface{}) {
b := c.h.buffers.New()
@ -228,9 +251,9 @@ func (s *Server) Users(c *Connection) { @@ -228,9 +251,9 @@ func (s *Server) Users(c *Connection) {
func (s *Server) Authenticate(c *Connection, st *SessionToken) bool {
err := c.Session.Authenticate(c.h.realm, st)
err := c.h.authenticateHandler(c.Session, st, "")
if err == nil {
log.Println("Authentication success", c.Id, c.Idx, st.Userid)
log.Println("Authentication success", c.Id, c.Idx, c.Session.Userid)
return true
} else {
log.Println("Authentication failed", err, c.Id, c.Idx, st.Userid, st.Nonce)

87
src/app/spreed-webrtc-server/session.go

@ -26,6 +26,7 @@ import ( @@ -26,6 +26,7 @@ import (
"fmt"
"github.com/gorilla/securecookie"
"sync"
"time"
)
var sessionNonces *securecookie.SecureCookie
@ -33,21 +34,23 @@ var sessionNonces *securecookie.SecureCookie @@ -33,21 +34,23 @@ var sessionNonces *securecookie.SecureCookie
type Session struct {
Id string
Sid string
Userid string
Roomid string
Ua string
UpdateRev uint64
Status interface{}
Nonce string
Prio int
mutex sync.RWMutex
userid string
stamp int64
}
func NewSession(id, sid, userid string) *Session {
func NewSession(id, sid string) *Session {
return &Session{
Id: id,
Sid: sid,
Userid: userid,
Id: id,
Sid: sid,
Prio: 100,
stamp: time.Now().Unix(),
}
}
@ -61,12 +64,12 @@ func (s *Session) Update(update *SessionUpdate) uint64 { @@ -61,12 +64,12 @@ func (s *Session) Update(update *SessionUpdate) uint64 {
//fmt.Println("type update", key)
switch key {
case "Roomid":
s.Roomid = update.Roomid
case "Ua":
s.Ua = update.Ua
case "Status":
s.Status = update.Status
case "Prio":
s.Prio = update.Prio
}
}
@ -76,19 +79,6 @@ func (s *Session) Update(update *SessionUpdate) uint64 { @@ -76,19 +79,6 @@ func (s *Session) Update(update *SessionUpdate) uint64 {
}
func (s *Session) Apply(st *SessionToken) uint64 {
s.mutex.Lock()
defer s.mutex.Unlock()
s.Id = st.Id
s.Sid = st.Sid
s.Userid = st.Userid
s.UpdateRev++
return s.UpdateRev
}
func (s *Session) Authorize(realm string, st *SessionToken) (string, error) {
s.mutex.Lock()
@ -97,7 +87,7 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) { @@ -97,7 +87,7 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) {
if s.Id != st.Id || s.Sid != st.Sid {
return "", errors.New("session id mismatch")
}
if s.Userid != "" {
if s.userid != "" {
return "", errors.New("session already authenticated")
}
@ -109,35 +99,41 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) { @@ -109,35 +99,41 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) {
}
func (s *Session) Authenticate(realm string, st *SessionToken) error {
func (s *Session) Authenticate(realm string, st *SessionToken, userid string) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.Userid != "" {
if s.userid != "" {
return errors.New("session already authenticated")
}
if s.Nonce == "" || s.Nonce != st.Nonce {
return errors.New("nonce validation failed")
}
var userid string
err := sessionNonces.Decode(fmt.Sprintf("%s@%s", s.Sid, realm), st.Nonce, &userid)
if err != nil {
return err
}
if st.Userid != userid {
return errors.New("user id mismatch")
if userid == "" {
if s.Nonce == "" || s.Nonce != st.Nonce {
return errors.New("nonce validation failed")
}
err := sessionNonces.Decode(fmt.Sprintf("%s@%s", s.Sid, realm), st.Nonce, &userid)
if err != nil {
return err
}
if st.Userid != userid {
return errors.New("user id mismatch")
}
s.Nonce = ""
}
s.Nonce = ""
s.Userid = st.Userid
s.userid = userid
s.stamp = time.Now().Unix()
s.UpdateRev++
return nil
}
func (s *Session) Token() *SessionToken {
return &SessionToken{Id: s.Id, Sid: s.Sid, Userid: s.Userid}
s.mutex.RLock()
defer s.mutex.RUnlock()
return &SessionToken{Id: s.Id, Sid: s.Sid, Userid: s.userid}
}
func (s *Session) Data() *DataSession {
@ -147,14 +143,22 @@ func (s *Session) Data() *DataSession { @@ -147,14 +143,22 @@ func (s *Session) Data() *DataSession {
return &DataSession{
Id: s.Id,
Userid: s.Userid,
Userid: s.userid,
Ua: s.Ua,
Status: s.Status,
Rev: s.UpdateRev,
Prio: s.Prio,
stamp: s.stamp,
}
}
func (s *Session) Userid() string {
return s.userid
}
func (s *Session) DataSessionLeft(state string) *DataSession {
s.mutex.RLock()
@ -176,8 +180,9 @@ func (s *Session) DataSessionJoined() *DataSession { @@ -176,8 +180,9 @@ func (s *Session) DataSessionJoined() *DataSession {
return &DataSession{
Type: "Joined",
Id: s.Id,
Userid: s.Userid,
Userid: s.userid,
Ua: s.Ua,
Prio: s.Prio,
}
}
@ -190,9 +195,10 @@ func (s *Session) DataSessionStatus() *DataSession { @@ -190,9 +195,10 @@ func (s *Session) DataSessionStatus() *DataSession {
return &DataSession{
Type: "Status",
Id: s.Id,
Userid: s.Userid,
Userid: s.userid,
Status: s.Status,
Rev: s.UpdateRev,
Prio: s.Prio,
}
}
@ -202,6 +208,7 @@ type SessionUpdate struct { @@ -202,6 +208,7 @@ type SessionUpdate struct {
Types []string
Roomid string
Ua string
Prio int
Status interface{}
}

112
src/app/spreed-webrtc-server/user.go

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"fmt"
"sort"
"sync"
)
type User struct {
Id string
sessionTable map[string]*Session
mutex sync.RWMutex
}
func NewUser(id string) *User {
user := &User{
Id: id,
sessionTable: make(map[string]*Session),
}
return user
}
// Return true if first session.
func (u *User) AddSession(s *Session) bool {
first := false
u.mutex.Lock()
u.sessionTable[s.Id] = s
if len(u.sessionTable) == 1 {
fmt.Println("First session registered for user", u.Id)
first = true
}
u.mutex.Unlock()
return first
}
// Return true if no session left.
func (u *User) RemoveSession(s *Session) bool {
last := false
u.mutex.Lock()
delete(u.sessionTable, s.Id)
if len(u.sessionTable) == 0 {
fmt.Println("Last session unregistered for user", u.Id)
last = true
}
u.mutex.Unlock()
return last
}
func (u *User) Data() *DataUser {
u.mutex.RLock()
defer u.mutex.RUnlock()
return &DataUser{
Id: u.Id,
Sessions: len(u.sessionTable),
}
}
func (u *User) SessionsData() []*DataSession {
sessions := make([]*DataSession, 0, len(u.sessionTable))
u.mutex.RLock()
defer u.mutex.RUnlock()
for _, session := range u.sessionTable {
sessions = append(sessions, session.Data())
}
sort.Sort(ByPrioAndStamp(sessions))
return sessions
}
type ByPrioAndStamp []*DataSession
func (a ByPrioAndStamp) Len() int {
return len(a)
}
func (a ByPrioAndStamp) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByPrioAndStamp) Less(i, j int) bool {
if a[i].Prio < a[j].Prio {
return true
}
if a[i].Prio == a[j].Prio {
return a[i].stamp < a[j].stamp
}
return false
}

5
src/app/spreed-webrtc-server/users.go

@ -74,10 +74,11 @@ func (uh *UsersSharedsecretHandler) Get(request *http.Request) (userid string, e @@ -74,10 +74,11 @@ func (uh *UsersSharedsecretHandler) Get(request *http.Request) (userid string, e
func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest, request *http.Request) (string, error) {
// Parse UseridCombo.
useridCombo := strings.SplitN(snr.UseridCombo, ":", 2)
if len(useridCombo) != 2 {
useridCombo := strings.SplitN(snr.UseridCombo, ":", 3)
if len(useridCombo) < 2 {
return "", errors.New("invalid useridcombo")
}
// TODO(longsleep): Add support for third field which provides the username.
expirationString, userid := useridCombo[0], useridCombo[1]
expiration, err := strconv.ParseInt(expirationString, 10, 64)

62
src/styles/components/_buddylist.scss

@ -113,7 +113,7 @@ @@ -113,7 +113,7 @@
cursor: pointer;
display: block;
font-size: 13px;
height: 66px;
min-height: 66px;
overflow: hidden;
position: relative;
text-align: left;
@ -126,15 +126,21 @@ @@ -126,15 +126,21 @@
}
.buddy {
&.withSubline .buddy1 {
&.withSubline .buddy1, &.contact .buddy1 {
top: 15px;
}
&.withSubline .buddy2 {
&.withSubline .buddy2, &.contact .buddy2 {
display: block;
}
&.hovered .buddyactions {
right: 0;
}
& .fa.contact:before {
content: "\f006";
}
&.contact .fa.contact:before {
content: "\f005";
}
.buddyPicture {
background: $actioncolor1;
border-radius: 2px;
@ -158,6 +164,16 @@ @@ -158,6 +164,16 @@
top: 0;
}
}
.buddyPictureSmall {
margin:0px;
margin-left:-4px;
height:30px;
width:30px;
margin-right:4px;
.#{$fa-css-prefix} {
line-height: 30px;
}
}
.buddy1 {
color: $componentfg1;
font-weight: bold;
@ -181,11 +197,19 @@ @@ -181,11 +197,19 @@
white-space: nowrap;
display: none;
}
.buddy3 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
width:160px;
text-align:left;
}
}
.buddy .buddyactions {
background: $buddylist-action-background;
bottom: 0px;
height: 66px;
line-height: 66px;
position: absolute;
right: -125px;
@ -199,4 +223,34 @@ @@ -199,4 +223,34 @@
.#{$fa-css-prefix} {
font-size: 2em;
}
i.fa {
pointer-events: none;
}
}
.buddy .buddysessions {
margin-top: 56px;
max-height:0px;
transition-property: max-height;
transition-duration: 1s;
transition-delay: .1s;
ul {
padding-top: 10px;
margin-left: 20px;
padding-left: 0px;
border-left: 1px dotted $bordercolor;
}
ul li {
margin-bottom: 1px;
margin-top: 1px;
margin-left: -4px;
list-style-type: none;
}
.currentsession .buddy3 {
font-weight: bold;
}
}
.buddy.hovered .buddysessions {
max-height:999px;
}

2
src/styles/components/_chat.scss

@ -439,7 +439,7 @@ @@ -439,7 +439,7 @@
}
}
.typinghint {
padding: 2px 6px 0 6px;
padding: 0 6px 0 6px;
white-space: nowrap;
overflow: hidden;
font-size:.8em;

1
src/styles/global/_base.scss

@ -131,4 +131,5 @@ a { @@ -131,4 +131,5 @@ a {
font-size: 1.1em;
margin-top: 80px;
text-shadow: 0 0 5px black;
max-width:500px;
}

49
static/js/controllers/chatroomcontroller.js

@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, moment, templateFileInfo) {
define(['underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/contactrequest.html'], function(_, moment, templateFileInfo, templateContactRequest) {
// ChatroomController
return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation) {
@ -45,6 +45,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome @@ -45,6 +45,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
var displayName = safeDisplayName;
var buddyImageSrc = $filter("buddyImageSrc");
var fileInfo = $compile(templateFileInfo);
var contactRequest = $compile(templateContactRequest);
var knowMessage = {
r: {},
@ -285,17 +286,17 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome @@ -285,17 +286,17 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
$scope.showmessage = function(from, timestamp, message, nodes) {
var userid = $scope.$parent.$parent.id;
var sessonid = $scope.$parent.$parent.id;
// Prepare message to display.
var s = [];
if (message) {
s.push(message);
$scope.$emit("incoming", message, from, userid);
$scope.$emit("incoming", message, from, sessonid);
}
var is_new_message = lastSender !== from;
var is_self = from === userid;
var is_self = from === sessonid;
var extra_css = "";
var title = null;
@ -373,7 +374,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome @@ -373,7 +374,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
$scope.$on("received", function(event, from, data) {
var userid = $scope.$parent.$parent.id;
var sessionid = $scope.$parent.$parent.id;
var mid = data.Mid || null;
switch (data.Type) {
@ -394,9 +395,10 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome @@ -394,9 +395,10 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
// Definitions.
var message = null;
var nodes = null;
var fromself = from === userid;
var fromself = from === sessionid;
var noop = false;
var element = null;
var subscope;
var timestamp = data.Time;
if (!timestamp) {
@ -441,7 +443,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome @@ -441,7 +443,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
// File offers.
if (data.Status.FileInfo) {
var subscope = $scope.$new();
subscope = $scope.$new();
subscope.info = data.Status.FileInfo;
subscope.from = from;
fileInfo(subscope, function(clonedElement, scope) {
@ -451,6 +453,39 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome @@ -451,6 +453,39 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
noop = true;
}
// Contact request.
if (data.Status.ContactRequest) {
subscope = $scope.$new();
subscope.request = data.Status.ContactRequest;
subscope.fromself = fromself;
contactRequest(subscope, function(clonedElement, scope) {
var text;
if (fromself) {
if (scope.request.Userid) {
if (scope.request.Success) {
text = translation._("You accepted the contact request.");
} else {
text = translation._("You rejected the contact request.");
}
} else {
text = translation._("You sent a contact request.");
}
} else {
if (scope.request.Success) {
text = translation._("Your contact request was accepted.");
} else{
if (scope.request.Token) {
text = translation._("Incoming contact request.");
} else {
text = translation._("Your contact request was rejected.");
}
}
}
element = $scope.showmessage(from, timestamp, text, clonedElement);
});
noop = true;
}
// Ignore unknown status messages.
if (message === null && nodes === null) {
noop = true;

6
static/js/controllers/controllers.js

@ -24,13 +24,15 @@ define([ @@ -24,13 +24,15 @@ define([
'controllers/mediastreamcontroller',
'controllers/statusmessagecontroller',
'controllers/chatroomcontroller',
'controllers/roomchangecontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, RoomchangeController) {
'controllers/roomchangecontroller',
'controllers/usersettingscontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, RoomchangeController, UsersettingsController) {
var controllers = {
MediastreamController: MediastreamController,
StatusmessageController: StatusmessageController,
ChatroomController: ChatroomController,
RoomchangeController: RoomchangeController
RoomchangeController: RoomchangeController,
UsersettingsController: UsersettingsController
};
var initialize = function(angModule) {

38
static/js/controllers/mediastreamcontroller.js

@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
*/
define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function(_, BigScreen, moment, sjcl) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage) {
/*console.log("route", $route, $routeParams, $location);*/
@ -123,6 +123,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -123,6 +123,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
$scope.status = "initializing";
$scope.id = null;
$scope.userid = null;
$scope.suserid = null;
$scope.peer = null;
$scope.dialing = null;
$scope.conference = null;
@ -138,6 +139,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -138,6 +139,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
$scope.master = {
displayName: null,
buddyPicture: null,
message: null,
settings: {
videoQuality: "high",
stereo: true,
@ -174,7 +176,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -174,7 +176,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
// This is the user status.
var status = {
displayName: $scope.master.displayName || null,
buddyPicture: $scope.master.buddyPicture || null
buddyPicture: $scope.master.buddyPicture || null,
message: $scope.master.message || null
}
if (_.isEqual(status, cache.status)) {
console.log("Status update skipped, as status has not changed.")
@ -344,7 +347,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -344,7 +347,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
});
// Load stuff from localStorage.
// TODO(longsleep): Put localStorage into Angular service.
var storedUser = localStorage.getItem("mediastream-user");
console.log("Found stored user data:", storedUser);
if (storedUser) {
@ -371,6 +373,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -371,6 +373,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
safeApply($scope, function(scope) {
scope.id = scope.myid = data.Id;
scope.userid = data.Userid;
scope.suserid = data.Suserid;
scope.turn = data.Turn;
scope.stun = data.Stun;
scope.refreshWebrtcSettings();
@ -402,6 +405,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -402,6 +405,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
delete data.nonce;
}, function(data, status) {
console.error("Failed to authorize session", status, data);
mediaStream.users.forget();
});
}
}
@ -427,6 +431,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -427,6 +431,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
}, 0);
}
appData.e.triggerHandler("selfReceived", data);
});
mediaStream.webrtc.e.on("peercall", function(event, peercall) {
@ -545,7 +551,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -545,7 +551,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
if (opts.soft) {
return;
}
$scope.userid = null;
$scope.userid = $scope.suserid = null;
break;
case "error":
if (reconnecting || connected) {
@ -621,9 +627,12 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -621,9 +627,12 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
});
$scope.$watch("userid", function(userid) {
var suserid;
if (userid) {
console.info("Session is now authenticated:", userid);
suserid = $scope.suserid;
console.info("Session is now authenticated:", userid, suserid);
}
appData.e.triggerHandler("authenticationChanged", [userid, suserid]);
});
// Apply all layout stuff as classes to our element.
@ -659,8 +668,23 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function @@ -659,8 +668,23 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
if (mediaStream.connector.connected) {
$scope.setStatus("waiting");
}
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
if ($scope.roomstatus) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
});
$scope.$watch("roomstatus", function(roomstatus) {
if (roomstatus && !$scope.peer) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else if (!$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
});
mediaStream.webrtc.e.on("busy", function(event, from) {

66
static/js/controllers/usersettingscontroller.js

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define([], function() {
// UsersettingsController
return ["$scope", "$element", "mediaStream", "safeApply", function($scope, $element, mediaStream, safeApply) {
$scope.withUsersForget = true;
this.registerUserid = function(btn) {
var successHandler = function(data) {
console.info("Created new userid:", data.userid);
// If the server provided us a nonce, we can do everthing on our own.
mediaStream.users.store(data);
$scope.loadedUserlogin = true;
safeApply($scope);
// Directly authenticate ourselves with the provided nonce.
mediaStream.api.requestAuthentication(data.userid, data.nonce);
delete data.nonce;
};
console.log("No userid - creating one ...");
mediaStream.users.register(btn.form, function(data) {
if (data.nonce) {
successHandler(data);
} else {
// No nonce received. So this means something we cannot do on our own.
// Make are GET request and retrieve nonce that way and let the
// browser/server do the rest.
mediaStream.users.authorize(data, successHandler, function(data, status) {
console.error("Failed to get nonce after create", status, data);
});
}
}, function(data, status) {
console.error("Failed to create userid", status, data);
});
};
this.forgetUserid = function() {
mediaStream.users.forget();
mediaStream.connector.forgetAndReconnect();
};
}];
});

26
static/js/directives/audiolevel.js

@ -18,11 +18,10 @@ @@ -18,11 +18,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['jquery', 'underscore', 'rAF'], function($, _) {
define(['jquery', 'underscore'], function($, _) {
return ["$window", "mediaStream", "safeApply", function($window, mediaStream, safeApply) {
return ["$window", "mediaStream", "safeApply", "animationFrame", function($window, mediaStream, safeApply, animationFrame) {
var requestAnimationFrame = $window.requestAnimationFrame;
var webrtc = mediaStream.webrtc;
// Consider anyting lower than this % as no audio.
@ -43,21 +42,22 @@ define(['jquery', 'underscore', 'rAF'], function($, _) { @@ -43,21 +42,22 @@ define(['jquery', 'underscore', 'rAF'], function($, _) {
// Own audio level indicator.
var element = $element[0];
var width = 0;
this.update = _.bind(function() {
if (this.active) {
requestAnimationFrame(this.update);
}
var width = 0;
if (webrtc.usermedia.audioLevel) {
width = Math.round(100 * webrtc.usermedia.audioLevel);
// Hide low volumes.
if (width < threshhold) {
if (this.active || width > 0) {
if (webrtc.usermedia.audioLevel) {
width = Math.round(100 * webrtc.usermedia.audioLevel);
// Hide low volumes.
if (width < threshhold) {
width = 0;
}
} else {
width = 0;
}
element.style.width = width + '%';
}
element.style.width = width + '%';
}, this);
this.update();
animationFrame.register(this.update);
// Talking state.
this.audioActivityHistory = [];

8
static/js/directives/audiovideo.js

@ -18,11 +18,10 @@ @@ -18,11 +18,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'injectCSS', 'webrtc.adapter', 'rAF'], function($, _, template, templatePeer, BigScreen) {
define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'injectCSS', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) {
return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout) {
return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", "animationFrame", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout, animationFrame) {
var requestAnimationFrame = $window.requestAnimationFrame;
var peerTemplate = $compile(templatePeer);
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
@ -316,9 +315,8 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ @@ -316,9 +315,8 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
needsRedraw = false;
redraw();
}
requestAnimationFrame(update);
}
_.defer(update);
animationFrame.register(update);
}

49
static/js/directives/buddylist.js

@ -21,14 +21,14 @@ @@ -21,14 +21,14 @@
define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
// buddyList
return ["$compile", "buddyList", "mediaStream", function($compile, buddyList, mediaStream) {
return ["$compile", "buddyList", "mediaStream", "contacts", function($compile, buddyList, mediaStream, contacts) {
//console.log("buddyList directive");
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.layout.buddylist = false;
$scope.enabled = false;
$scope.layout.buddylistAutoHide = true;
$scope.doCall = function(id) {
@ -46,33 +46,44 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -46,33 +46,44 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
};
$scope.doContactRequest = function(id) {
//console.log("doContact", id);
$scope.$emit("requestcontact", id, {
restore: true
});
};
$scope.doContactRemove = function(userid) {
contacts.remove(userid);
};
/*
$scope.doAudioConference = function(id) {
$scope.updateAutoAccept(id);
mediaStream.api.sendChat(id, null, {
type: "conference",
id: mediaStream.connector.roomid
AutoCall: {
Type: "conference",
Id: mediaStream.connector.roomid
}
})
};
};*/
$scope.setRoomStatus = function(status) {
if (status !== $scope.enabled) {
$scope.enabled = status;
$scope.$emit("roomStatus", status);
}
if (status && !$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = true
}
$scope.$emit("roomStatus", status);
};
//XXX(longsleep): Debug leftover ?? Remove this.
window.doAudioConference = $scope.doAudioConference;
var buddylist = $scope.buddylist = buddyList.buddylist($element, $scope, {});
var onJoined = _.bind(buddylist.onJoined, buddylist);
var onLeft = _.bind(buddylist.onLeft, buddylist);
var onStatus = _.bind(buddylist.onStatus, buddylist);
var onContactAdded = _.bind(buddylist.onContactAdded, buddylist);
var onContactRemoved = _.bind(buddylist.onContactRemoved, buddylist);
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) {
if (dataType === "Left") {
onLeft(data);
@ -97,11 +108,17 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -97,11 +108,17 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
$scope.setRoomStatus(false);
buddylist.onClosed();
});
// Request user list whenever the connection comes ready.
mediaStream.connector.ready(function() {
mediaStream.api.requestUsers();
});
// Contacts.
contacts.e.on("contactadded", function(event, data) {
onContactAdded(data);
});
contacts.e.on("contactremoved", function(event, data) {
onContactRemoved(data);
});
}];

103
static/js/directives/chat.js

@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
*/
define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function(_, templateChat, templateChatroom) {
return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "$timeout", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, $timeout) {
return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "appData", "$timeout", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, appData, $timeout) {
var displayName = safeDisplayName;
var group_chat_id = "";
@ -138,6 +138,33 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -138,6 +138,33 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
});
$scope.$parent.$on("requestcontact", function(event, id, options) {
if (id !== group_chat_id) {
// Make sure the contact id is valid.
var ad = appData.get();
if (!ad.userid) {
// Unable to add contacts as we have no own userid.
console.log("You need to log in to add contacts.");
return;
}
var bd = buddyData.get(id);
if (!bd || !bd.session || !bd.session.Userid || ad.userid === bd.session.Userid) {
console.log("This contact cannot be added.");
return;
}
var subscope = $scope.showRoom(id, {
title: translation._("Chat with")
}, options);
subscope.sendChatServer(id, "Contact request", {
ContactRequest: {
Id: randomGen.random({hex: true})
}
});
}
});
}];
var compile = function(tElement, tAttrs) {
@ -205,47 +232,59 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -205,47 +232,59 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
subscope.sendChat = function(to, message, status, mid, noloop) {
//console.log("send chat", to, scope.peer);
var peercall = mediaStream.webrtc.findTargetCall(to);
if (message && !mid) {
mid = randomGen.random({
hex: true
});
}
if (peercall && peercall.peerconnection.datachannelReady) {
subscope.p2p(true);
// Send out stuff through data channel.
_.delay(function() {
mediaStream.api.apply("sendChat", {
send: function(type, data) {
// We also send to self, to display our own stuff.
if (!noloop) {
mediaStream.api.received({
Type: data.Type,
Data: data,
From: mediaStream.api.id,
To: peercall.id
});
}
return peercall.peerconnection.send(data);
}
})(to, message, status, mid);
}, 100);
return subscope.sendChatPeer2Peer(peercall, to, message, status, mid, noloop);
} else {
subscope.p2p(false);
_.delay(function() {
mediaStream.api.send2("sendChat", function(type, data) {
return subscope.sendChatServer(to, message, status, mid, noloop);
}
return mid;
};
subscope.sendChatPeer2Peer = function(peercall, to, message, status, mid, noloop) {
if (message && !mid) {
mid = randomGen.random({
hex: true
});
}
_.delay(function() {
mediaStream.api.apply("sendChat", {
send: function(type, data) {
// We also send to self, to display our own stuff.
if (!noloop) {
//console.log("looped to self", type, data);
mediaStream.api.received({
Type: data.Type,
Data: data,
From: mediaStream.api.id,
To: to
To: peercall.id
});
}
})(to, message, status, mid);
}, 100);
return peercall.peerconnection.send(data);
}
})(to, message, status, mid);
}, 100);
return mid;
};
subscope.sendChatServer = function(to, message, status, mid, noloop) {
if (message && !mid) {
mid = randomGen.random({
hex: true
});
}
_.delay(function() {
mediaStream.api.send2("sendChat", function(type, data) {
if (!noloop) {
//console.log("looped to self", type, data);
mediaStream.api.received({
Type: data.Type,
Data: data,
From: mediaStream.api.id,
To: to
});
}
})(to, message, status, mid);
}, 100);
return mid;
};
subscope.p2p = function(state) {
@ -295,8 +334,8 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -295,8 +334,8 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
}
safeApply(subscope);
});
subscope.$on("incoming", function(event, message, from, userid) {
if (from !== userid) {
subscope.$on("incoming", function(event, message, from, sessionid) {
if (from !== sessionid) {
subscope.pending++;
scope.$emit("chatincoming", subscope.id);
}
@ -304,7 +343,7 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -304,7 +343,7 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
var room = event.targetScope.id;
// Make sure we are not in group chat or the message is from ourselves
// before we beep and shout.
if (!subscope.isgroupchat && from !== userid) {
if (!subscope.isgroupchat && from !== sessionid) {
playSound.play("message1");
desktopNotify.notify(translation._("Message from ") + displayName(from), message);
}

72
static/js/directives/contactrequest.js

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['jquery', 'underscore'], function($, _) {
return ["translation", "buddyData", "contacts", function(translation, buddyData, contacts) {
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.state = "request";
$scope.doAccept = function() {
$scope.state = "accepted";
$scope.doContact(true);
};
$scope.doReject = function() {
$scope.state = "rejected";
$scope.doContact(false);
};
$scope.doContact = function(success) {
var r = $scope.request;
r.Success = !!success;
$scope.sendChatServer($scope.id, "Contact request answer", {
ContactRequest: r
});
};
$scope.addContact = function(request, status) {
contacts.add(request, status)
};
// Add support for contacts on controller creation.
var request = $scope.request;
if (request.Success && request.Userid && request.Token) {
var buddy = buddyData.lookup($scope.id);
var status = {};
if (buddy) {
$.extend(status, buddy.status);
}
$scope.addContact(request, status);
}
}];
return {
scope: true,
restrict: 'EAC',
controller: controller,
replace: false
}
}];
});

6
static/js/directives/directives.js

@ -35,7 +35,8 @@ define([ @@ -35,7 +35,8 @@ define([
'directives/screenshare',
'directives/roombar',
'directives/socialshare',
'directives/page'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPicture, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page) {
'directives/page',
'directives/contactrequest'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPicture, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest) {
var directives = {
onEnter: onEnter,
@ -52,7 +53,8 @@ define([ @@ -52,7 +53,8 @@ define([
screenshare: screenshare,
roomBar: roomBar,
socialShare: socialShare,
page: page
page: page,
contactRequest: contactRequest
};
var initialize = function(angModule) {

38
static/js/directives/settings.js

@ -22,7 +22,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -22,7 +22,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
return ["$compile", "mediaStream", function($compile, mediaStream) {
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation) {
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', 'localStorage', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation, localStorage) {
$scope.layout.settings = false;
$scope.showAdvancedSettings = true;
@ -74,42 +74,6 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -74,42 +74,6 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
});
};
$scope.registerUserid = function(btn) {
var successHandler = function(data) {
console.info("Created new userid:", data.userid);
// If the server provided us a nonce, we can do everthing on our own.
mediaStream.users.store(data);
$scope.loadedUserlogin = true;
safeApply($scope);
// Directly authenticate ourselves with the provided nonce.
mediaStream.api.requestAuthentication(data.userid, data.nonce);
delete data.nonce;
};
console.log("No userid - creating one ...");
mediaStream.users.register(btn.form, function(data) {
if (data.nonce) {
successHandler(data);
} else {
// No nonce received. So this means something we cannot do on our own.
// Make are GET request and retrieve nonce that way and let the
// browser/server do the rest.
mediaStream.users.authorize(data, successHandler, function(data, status) {
console.error("Failed to get nonce after create", status, data);
});
}
}, function(data, status) {
console.error("Failed to create userid", status, data);
});
};
$scope.forgetUserid = function() {
mediaStream.users.forget();
mediaStream.connector.forgetAndReconnect();
};
$scope.checkDefaultMediaSources = function() {
if ($scope.master.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.master.settings.microphoneId)) {
$scope.master.settings.microphoneId = null;

2
static/js/directives/usability.js

@ -24,7 +24,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, @@ -24,7 +24,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
return ["mediaStream", function(mediaStream) {
var controller = ['$scope', "mediaStream", "safeApply", "$timeout", function($scope, mediaStream, safeApply, $timeout) {
var controller = ['$scope', "mediaStream", "safeApply", "$timeout", "localStorage", function($scope, mediaStream, safeApply, $timeout, localStorage) {
var pending = true;
var complete = false;

14
static/js/filters/buddyimagesrc.js

@ -79,18 +79,18 @@ define(["underscore"], function(_) { @@ -79,18 +79,18 @@ define(["underscore"], function(_) {
var scope = buddyData.lookup(id);
if (scope) {
var status = scope.status;
if (status) {
if (status.buddyPictureLocalUrl) {
return status.buddyPictureLocalUrl;
} else if (status.buddyPicture) {
var display = scope.display;
if (display) {
if (display.buddyPictureLocalUrl) {
return display.buddyPictureLocalUrl;
} else if (display.buddyPicture) {
var url = urls[id];
if (url) {
revokeURL(id, url);
}
// New data -> new url.
var blob = dataURLToBlob(status.buddyPicture);
url = status.buddyPictureLocalUrl = urls[id] = blobToObjectURL(blob);
var blob = dataURLToBlob(display.buddyPicture);
url = display.buddyPictureLocalUrl = urls[id] = blobToObjectURL(blob);
return url;
}
}

4
static/js/filters/displayname.js

@ -35,8 +35,8 @@ define([], function() { @@ -35,8 +35,8 @@ define([], function() {
}
var scope = buddyData.lookup(id);
if (scope) {
if (scope.displayName) {
return scope.displayName;
if (scope.display.displayName) {
return scope.display.displayName;
}
return user_text + " " + scope.buddyIndex;
} else {

39
static/js/mediastream/api.js

@ -29,6 +29,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -29,6 +29,7 @@ define(['jquery', 'underscore'], function($, _) {
this.sid = null;
this.session = {};
this.connector = connector;
this.iids= 0;
this.e = $({});
@ -74,7 +75,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -74,7 +75,7 @@ define(['jquery', 'underscore'], function($, _) {
Type: type
};
payload[type] = data;
//console.log("<<<<<<<<<<<<", JSON.stringify(payload));
//console.log("<<<<<<<<<<<<", JSON.stringify(payload, null, 2));
this.connector.send(payload, noqueue);
};
@ -91,6 +92,21 @@ define(['jquery', 'underscore'], function($, _) { @@ -91,6 +92,21 @@ define(['jquery', 'underscore'], function($, _) {
return this.apply(name, obj);
};
Api.prototype.request = function(type, data, cb) {
var payload = {
Type: type
}
payload[type] = data;
if (cb) {
var iid = ""+(this.iids++);
payload.Iid = iid;
this.e.one(iid+".request", cb);
}
this.connector.send(payload);
}
// Helper hack function to send API requests to other destinations.
// Simply provide an alternative send function on the obj Object.
Api.prototype.apply = function(name, obj) {
@ -105,9 +121,16 @@ define(['jquery', 'underscore'], function($, _) { @@ -105,9 +121,16 @@ define(['jquery', 'underscore'], function($, _) {
this.last_receive = now;
this.last_receive_overdue = false;
var iid = d.Iid;
var data = d.Data;
var dataType = data.Type;
if (iid) {
// Shortcut for iid registered responses.
this.e.triggerHandler(iid+".request", [dataType, data]);
return;
}
switch (dataType) {
case "Self":
console.log("Self received", data);
@ -321,6 +344,20 @@ define(['jquery', 'underscore'], function($, _) { @@ -321,6 +344,20 @@ define(['jquery', 'underscore'], function($, _) {
return this.send("Alive", data);
};
Api.prototype.sendSessions = function(token, type, cb) {
var data = {
Type: "Sessions",
Sessions: {
Type: type,
Token: token
}
}
return this.request("Sessions", data, cb);
};
return Api;
});

51
static/js/services/animationframe.js

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(["underscore", "rAF"], function(_) {
// animationFrame
return ["$window", function($window) {
var requestAnimationFrame = $window.requestAnimationFrame;
var registry = [];
var caller = function(f) {
f();
};
var worker = function() {
registry.forEach(caller)
requestAnimationFrame(worker);
};
// Public api.
var animationFrame = {
register: function(f) {
registry.push(f);
}
};
// Auto start worker.
_.defer(worker);
return animationFrame;
}];
});

12
static/js/services/appdata.js

@ -18,13 +18,18 @@ @@ -18,13 +18,18 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define([], function() {
define(["jquery"], function($) {
// appData.e events
// - authenticationChanged(userid)
// - selfReceived(self)
// appData
return [function() {
var data = {
data: null
data: null,
e: $({})
}
var appData = {
get: function() {
@ -33,7 +38,8 @@ define([], function() { @@ -33,7 +38,8 @@ define([], function() {
set: function(d) {
data.data = d;
return d;
}
},
e: data.e
}
return appData;

33
static/js/services/buddydata.js

@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
define(['underscore'], function(underscore) {
// buddyData
return [function() {
return ["contactData", function(contactData) {
var scopes = {};
var brain = {};
@ -61,16 +61,35 @@ define(['underscore'], function(underscore) { @@ -61,16 +61,35 @@ define(['underscore'], function(underscore) {
}
return 0;
},
get: function(id, createInParent, afterCreateCallback) {
get: function(id, createInParent, afterCreateCallback, userid) {
if (scopes.hasOwnProperty(id)) {
//console.log("found id scope", id);
return scopes[id];
} else if (!createInParent && pushed.hasOwnProperty(id)) {
return pushed[id].scope;
} else {
var scope;
if (userid && scopes.hasOwnProperty(userid)) {
scope = scopes[userid];
if (createInParent) {
scopes[id] = scope;
}
//console.log("found userid scope", userid);
return scope;
}
if (createInParent) {
//console.log("creating scope", id, userid);
// If we have a parent we can create a new scope.
var scope = scopes[id] = createInParent.$new();
scope = scopes[id] = createInParent.$new();
if (userid) {
scopes[userid] = scope;
}
scope.buddyIndex = ++count;
if (userid) {
scope.contact = contactData.get(userid);
} else {
scope.contact = null;
}
scope.buddyIndexSortable = ("0000000" + scope.buddyIndex).slice(-7);
if (pushed.hasOwnProperty(id)) {
// Refresh pushed scope reference.
@ -101,13 +120,17 @@ define(['underscore'], function(underscore) { @@ -101,13 +120,17 @@ define(['underscore'], function(underscore) {
del: function(id, hard) {
var scope = scopes[id];
if (scope) {
scope.$destroy();
brain[id] = scope;
if (!hard) {
brain[id] = scope;
}
delete scopes[id];
return scope;
} else {
return null;
}
},
set: function(id, scope) {
scopes[id] = scope;
}
};
return buddyData;

464
static/js/services/buddylist.js

@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!partials/buddyactions.html', 'text!partials/buddyactionsforaudiomixer.html', 'rAF'], function(_, Modernizr, AvlTree, templateBuddy, templateBuddyActions, templateBuddyActionsForAudioMixer) {
define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!partials/buddyactions.html', 'text!partials/buddyactionsforaudiomixer.html'], function(_, Modernizr, AvlTree, templateBuddy, templateBuddyActions, templateBuddyActionsForAudioMixer) {
var BuddyTree = function() {
@ -31,7 +31,8 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -31,7 +31,8 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
BuddyTree.prototype.create = function(id, scope) {
var sort = scope.displayName ? scope.displayName : "session " + scope.buddyIndexSortable + " " + id;
var display = scope.display || {};
var sort = display.displayName ? display.displayName : "session " + scope.buddyIndexSortable + " " + id;
var data = {
id: id,
sort: sort + "z" + id
@ -66,6 +67,12 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -66,6 +67,12 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
BuddyTree.prototype.check = function(id) {
return this.data.hasOwnProperty(id);
};
/**
* Returns undefined when no change required. Position result otherwise.
*/
@ -102,6 +109,18 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -102,6 +109,18 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
BuddyTree.prototype.traverse = function(cb) {
return this.tree.inOrderTraverse(cb);
};
BuddyTree.prototype.keys = function() {
return _.keys(this.data);
};
BuddyTree.prototype.clear = function() {
this.tree.clear();
@ -110,9 +129,7 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -110,9 +129,7 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
// buddyList
return ["$window", "$compile", "playSound", "buddyData", "fastScroll", "mediaStream", function($window, $compile, playSound, buddyData, fastScroll, mediaStream) {
var requestAnimationFrame = $window.requestAnimationFrame;
return ["$window", "$compile", "playSound", "buddyData", "buddySession", "fastScroll", "mediaStream", "animationFrame", "$q", function($window, $compile, playSound, buddyData, buddySession, fastScroll, mediaStream, animationFrame, $q) {
var buddyTemplate = $compile(templateBuddy);
var buddyActions = $compile(templateBuddyActions);
@ -138,25 +155,24 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -138,25 +155,24 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
$element.on("mouseenter mouseleave", ".buddy", _.bind(function(event) {
// Hover handler for on Buddy actions.
var buddyElement = $(event.currentTarget);
this.hover(buddyElement, event.type === "mouseenter" ? true : false, buddyElement.scope().session.Id);
this.hover(buddyElement, event.type === "mouseenter" ? true : false);
}, this));
$element.on("click", ".buddy", _.bind(function(event) {
var buddyElement = $(event.currentTarget);
buddyElement.scope().doDefault();
this.click(buddyElement, event.target);
}, this));
$element.attr("data-xthreshold", "10");
$element.on("swipeleft", ".buddy", _.bind(function(event) {
event.preventDefault();
var buddyElement = $(event.currentTarget);
this.hover(buddyElement, !buddyElement.hasClass("hovered"), buddyElement.scope().session.Id);
this.hover(buddyElement, !buddyElement.hasClass("hovered"));
}, this));
$window.setInterval(_.bind(this.soundLoop, this), 500);
var update = _.bind(function refreshBuddies() {
this.refreshBuddies();
requestAnimationFrame(update);
}, this);
requestAnimationFrame(update);
animationFrame.register(update);
};
@ -185,23 +201,63 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -185,23 +201,63 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
Buddylist.prototype.onBuddyScope = function(scope) {
Buddylist.prototype.onBuddyScopeCreated = function(scope, data) {
// Init scope with our stuff.
scope.element = null;
scope.doDefault = function() {
var id = scope.session.Id;
if (scope.status.isMixer) {
return scope.doAudioConference(id);
}
return scope.doCall(id);
};
scope.contact = null;
scope.display = {};
scope.session = buddySession.create(data);
scope.$on("$destroy", function() {
//console.log("destroyed");
scope.element = null;
scope.killed = true;
});
};
Buddylist.prototype.onBuddySessionUserid = function(scope, sourceSession) {
//console.log("session with userid", sourceSession);
var userid = sourceSession.Userid;
/*
if (userid === scope.userid) {
// The source session has our own userid, ignore it.
}*/
var targetScope = buddyData.get(userid);
if (!targetScope) {
// No scope for this userid yet - set us.
buddyData.set(userid, scope);
//console.log("set scope with userid", sourceSession);
return;
}
var session = targetScope.session;
if (sourceSession === session) {
// No action.
//console.log("source session same as target");
return;
}
// Merge sessions.
session.merge(sourceSession);
// Cleanup old from tree and DOM.
var id = sourceSession.Id;
this.tree.remove(id);
if (targetScope !== scope) {
if (scope.element) {
this.lefts[id] = scope.element;
//console.log("destroying", id, scope.element);
scope.$destroy();
}
buddyData.set(id, targetScope);
delete this.actionElements[id];
return targetScope;
}
return;
};
Buddylist.prototype.soundLoop = function() {
if (this.playSoundLeft) {
@ -335,72 +391,160 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -335,72 +391,160 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
Buddylist.prototype.dumpBuddyPictureToBlob = function(scope, data) {
if (!data) {
data = this.dumpBuddyPictureToString(scope);
if (!data) {
return null;
}
}
// NOTE(longsleep): toBlob is not widely supported narf ..
// see: https://code.google.com/p/chromium/issues/detail?id=67587
var parts = data.match(/data:([^;]*)(;base64)?,([0-9A-Za-z+\/]+)/);
var binStr = atob(parts[3]);
var buf = new ArrayBuffer(binStr.length);
var view = new Uint8Array(buf);
for (var i = 0; i < view.length; i++) {
view[i] = binStr.charCodeAt(i);
}
return new Blob([view], {'type': parts[1]});
};
Buddylist.prototype.dumpBuddyPictureToString = function(scope) {
var img = scope.element.find(".buddyPicture img").get(0);
if (img) {
var canvas = $window.document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
return canvas.toDataURL("image/jpeg");
}
return null;
};
Buddylist.prototype.setDisplay = function(id, scope, data, queueName) {
var status = data.Status;
var display = scope.display;
// Set display.name.
display.displayName = status.displayName;
// Set display.picture.
display.buddyPicture = status.buddyPicture;
this.updateBuddyPicture(display);
// Set display subline.
this.updateSubline(display, status.message);
// Add to render queue when no element exists.
if (!scope.element) {
var before = this.tree.add(id, scope);
this.queue.push([queueName, id, before]);
this.playSoundJoined = true;
}
};
Buddylist.prototype.updateDisplay = function(id, scope, data, queueName) {
var status = data.Status;
var display = scope.display;
// Update display name.
var displayName = display.displayName;
if (status.displayName) {
display.displayName = status.displayName;
} else {
display.displayName = null;
}
// Add to status queue if sorting has changed.
if (displayName !== status.displayName) {
var before = this.tree.update(id, scope);
this.queue.push([queueName, id, before]);
}
// Update display subline.
if (status.message) {
this.updateSubline(display, status.message);
}
// Update display picture.
if (status.buddyPicture) {
display.buddyPicture = status.buddyPicture || null;
this.updateBuddyPicture(display);
}
};
Buddylist.prototype.updateSubline = function(display, s) {
if (!s || s === "__e") {
display.subline = "";
return;
}
if (s.length > 20) {
display.sublineFull = s;
s = s.substr(0, 20) + "...";
} else {
display.sublineFull = null;
}
display.subline = s;
};
Buddylist.prototype.onStatus = function(data) {
//console.log("onStatus", status);
//console.log("onStatus", data);
var id = data.Id;
var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
var scope = buddyData.get(id, this.$scope, _.bind(function(scope) {
this.onBuddyScopeCreated(scope, data);
}, this), data.Userid);
// Update session.
scope.session.Userid = data.Userid;
scope.session.Rev = data.Rev;
// Update status.
if (scope.status && scope.status.Rev >= data.Rev) {
console.warn("Received old status update in status", data.Rev, scope.status.Rev);
} else {
scope.status = data.Status;
this.updateBuddyPicture(scope.status);
var displayName = scope.displayName;
if (scope.status.displayName) {
scope.displayName = scope.status.displayName;
} else {
scope.displayName = null;
}
if (displayName !== scope.displayName) {
var before = this.tree.update(id, scope);
this.queue.push(["status", id, before]);
var sessionData = scope.session.update(id, data, _.bind(function(session) {
//console.log("Session is now authenticated", session);
var newscope = this.onBuddySessionUserid(scope, session);
if (newscope) {
scope = newscope;
}
scope.$apply();
}, this));
if (sessionData) {
// onStatus for main session.
this.updateDisplay(id, scope, sessionData, "status");
}
scope.$apply();
return scope;
};
Buddylist.prototype.onJoined = function(data) {
Buddylist.prototype.onJoined = function(data, noApply) {
//console.log("Joined", data);
var id = data.Id;
var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
// Create session.
scope.session = {
Id: data.Id,
Userid: data.Userid,
Ua: data.Ua,
Rev: 0
};
// Add status.
var scope = buddyData.get(id, this.$scope, _.bind(function(scope) {
this.onBuddyScopeCreated(scope, data);
}, this), data.Userid);
// Update session.
buddyCount++;
if (data.Status) {
if (scope.status && scope.status.Rev >= data.Status.Rev) {
console.warn("Received old status update in join", data.Status.Rev, scope.status.Rev);
} else {
scope.status = data.Status;
scope.displayName = scope.status.displayName;
this.updateBuddyPicture(scope.status);
var sessionData = scope.session.update(id, data, _.bind(function(session) {
//console.log("Session is now authenticated", session);
var newscope = this.onBuddySessionUserid(scope, session);
if (newscope) {
scope = newscope;
}
}, this));
if (sessionData && sessionData.Status) {
this.setDisplay(id, scope, sessionData, "joined");
} else if (!noApply) {
scope.$apply();
}
//console.log("Joined scope", scope, scope.element);
if (!scope.element) {
var before = this.tree.add(id, scope);
this.queue.push(["joined", id, before]);
this.playSoundJoined = true;
}
return scope;
};
Buddylist.prototype.onLeft = function(data) {
//console.log("Left", session);
Buddylist.prototype.onLeft = function(data, force, noApply) {
//console.log("Left", data);
var id = data.Id;
this.tree.remove(id);
var scope = buddyData.get(id);
if (!scope) {
//console.warn("Trying to remove buddy with no registered scope", session);
@ -409,30 +553,191 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -409,30 +553,191 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
if (buddyCount > 0) {
buddyCount--;
}
if (scope.element) {
this.lefts[id] = scope.element;
this.playSoundLeft = true;
}
// Remove current id from tree.
this.tree.remove(id);
buddyData.del(id);
delete this.actionElements[id];
// Remove session.
var session = scope.session;
if ((session.remove(id) && scope.contact === null) || force) {
// No session left. Cleanup.
if (scope.element) {
this.lefts[id] = scope.element;
this.playSoundLeft = true;
}
if (session.Userid) {
buddyData.del(session.Userid, true);
}
delete this.actionElements[id];
scope.$destroy();
} else {
// Update display stuff if a session is left. This can
// return no session in case when we got this as contact.
var sessionData = session.get();
if (sessionData) {
this.updateDisplay(sessionData.Id, scope, sessionData, "status");
} else if (scope.contact) {
var contact = scope.contact;
// Use it with userid as id in tree.
if (!this.tree.check(contact.Userid)) {
this.tree.add(contact.Userid, scope);
buddyCount++;
}
if (contact.Status !== null && !contact.Status.message) {
// Remove status message.
contact.Status.message = "__e";
}
this.updateDisplay(contact.Userid, scope, contact, "status");
}
if (!noApply) {
scope.$apply();
}
}
return scope;
};
Buddylist.prototype.onClosed = function() {
//console.log("Closed");
this.$element.empty();
buddyCount = 0;
buddyData.clear();
this.tree.clear();
this.actionElements = {};
// Remove pending stuff from queue.
this.queue = [];
// Trigger left events for all sessions.
var data = {};
var sessions = buddySession.sessions();
for (var id in sessions) {
if (sessions.hasOwnProperty(id)) {
//console.log("close id", id);
data.Id = id;
this.onLeft(data, false, true);
}
}
};
Buddylist.prototype.onContactAdded = function(contact) {
//console.log("onContactAdded", contact);
var userid = contact.Userid;
var scope = buddyData.get(userid);
if (scope) {
scope.contact = contact;
var sessionData = scope.session.get();
if (sessionData) {
if (contact.Status === null && sessionData.Status) {
// Update contact status with session.Status
var status = contact.Status = _.extend({}, sessionData.Status);
// Remove status message.
delete status.message;
// Convert buddy image.
if (status.buddyPicture) {
var img = this.dumpBuddyPictureToString(scope);
if (img) {
status.buddyPicture = img;
} else {
delete status.buddyPicture;
}
}
console.log("Injected status into contact", contact);
}
this.updateDisplay(sessionData.Id, scope, contact, "status");
scope.$apply();
}
} else {
// Create new scope for contact.
scope = this.onJoined({
Id: contact.Userid,
Userid: contact.Userid,
Status: _.extend({}, contact.Status)
});
scope.contact = contact;
}
};
Buddylist.prototype.hover = function(buddyElement, hover, id) {
Buddylist.prototype.onContactRemoved = function(contact) {
//console.log("onContactRemoved", contact);
var userid = contact.Userid;
var scope = buddyData.get(userid);
if (scope) {
scope.contact = null;
// Remove with left when no session for this userid.
var sessionData = scope.session.get();
if (!sessionData) {
// Force left.
this.onLeft({Id: userid}, true);
}
}
};
Buddylist.prototype.click = function(buddyElement, target) {
//console.log("click handler", buddyElement, target);
var action = $(target).data("action");
if (!action) {
// Make call the default action.
action = "call";
}
//console.log("hover handler", event, hover, id);
var scope = buddyElement.scope();
var session = scope.session;
var contact = scope.contact;
var promise = (function() {
var deferred = $q.defer();
var sessionData = session.get()
if (!sessionData) {
// Find session with help of contact.
if (contact && contact.Token) {
mediaStream.api.sendSessions(contact.Token, "contact", function(event, type, data) {
//console.log("oooooooooooooooo", type, data);
if (data.Users && data.Users.length > 0) {
var s = data.Users[0];
buddyData.set(s.Id, scope);
deferred.resolve(s.Id);
}
});
}
} else {
deferred.resolve(sessionData.Id);
}
return deferred.promise;
})();
switch (action) {
case "call":
promise.then(function(id) {
scope.doCall(id);
});
break;
case "chat":
promise.then(function(id) {
scope.doChat(id);
});
break;
case "contact":
if (contact) {
scope.doContactRemove(contact.Userid);
} else {
promise.then(function(id) {
scope.doContactRequest(id);
});
}
break;
}
};
Buddylist.prototype.hover = function(buddyElement, hover) {
//console.log("hover handler", buddyElement, hover);
var scope = buddyElement.scope();
var id = scope.session.Id;
var buddy = $(buddyElement);
var actionElements = this.actionElements;
var elem;
@ -453,11 +758,10 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text! @@ -453,11 +758,10 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
if (elem) {
buddy.addClass("hovered");
} else {
var scope = buddyData.get(id);
var template = buddyActions;
if (scope.status.isMixer) {
template = buddyActionsForAudioMixer;
}
//if (scope.status.autoCalls && _.indexOf(scope.status.autoCalls, "conference") !== -1) {
// template = buddyActionsForAudioMixer;
//}
//console.log("scope", scope, id);
template(scope, _.bind(function(clonedElement, $scope) {
actionElements[id] = clonedElement;

191
static/js/services/buddysession.js

@ -0,0 +1,191 @@ @@ -0,0 +1,191 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(["underscore"], function(_) {
// buddySession
return [function() {
var sessions = {};
var serials = 0;
var BuddySession = function(data) {
this.serial = serials++;
this.sessions = {};
this.count = 0;
//console.log("creating session", this.serial, data.Id, data.Userid, this);
var id = data.Id;
if (data.Id) {
var userid = data.Userid || null;
if (id === userid) {
// Add as default with userid.
this.use(userid, data);
} else {
// Add as session.
var sessionData = this.add(id, data);
if (userid) {
this.auth(userid, sessionData);
}
}
} else {
this.use(null, {});
}
};
BuddySession.prototype.add = function(id, data) {
this.sessions[id] = data;
if (this.count === 0) {
this.use(id, data);
}
this.count++;
sessions[id] = this;
return data;
};
BuddySession.prototype.rm = function(id) {
delete this.sessions[id];
this.count--;
delete sessions[id];
};
BuddySession.prototype.get = function(id) {
if (!id) {
id = this.Id;
}
return this.sessions[id];
};
BuddySession.prototype.use = function(id, data) {
if (id) {
this.Id = id;
} else {
this.Id = null;
}
//console.log("Use session as default", id, data, this);
};
BuddySession.prototype.remove = function(id, onEmptyCallback) {
this.rm(id);
if (id === this.Id) {
var sessions = this.sessions;
var sessionData;
for (var sd in sessions) {
if (sessions.hasOwnProperty(sd)) {
sessionData = sessions[sd];
break;
}
}
if (sessionData) {
this.use(sessionData.Id, sessionData);
} else {
//console.log("Last session removed", sessions);
if (this.Userid) {
//console.log("Using userid as session id");
this.use(this.Userid);
} else {
this.use(null);
}
return true;
}
}
return false;
};
BuddySession.prototype.update = function(id, data, onUseridCallback) {
var userid = data.Userid;
//console.log("session update", id, userid, this, data);
var sessionData
if (id === userid) {
// Fake updates from userid ids.
sessionData = data;
} else {
sessionData = this.get(id);
if (!sessionData) {
sessionData = this.add(id, data);
}
}
if (userid) {
this.auth(userid, sessionData, onUseridCallback);
}
if (data !== sessionData) {
if (data.Rev) {
sessionData.Rev = data.Rev;
}
if (data.Status) {
sessionData.Status = data.Status;
}
}
if (id === this.Id) {
return sessionData;
} else {
return null;
}
};
BuddySession.prototype.auth = function(userid, sessionData, onUseridCallback) {
if (!this.Userid) {
this.Userid = userid;
//console.log("Session now has a user id", this.Id, userid);
}
// Trigger callback if defined and not triggered before.
if (onUseridCallback && !sessionData.auth) {
onUseridCallback(this);
sessionData.auth = true;
}
};
BuddySession.prototype.merge = function(otherSession) {
if (!this.Userid) {
console.error("Refusing to merge into session as we have no userid", this, otherSession);
return;
}
if (otherSession.Userid !== this.Userid) {
console.error("Refusing to merge other session with different userid", otherSession, this);
return;
}
_.each(otherSession.sessions, _.bind(function(s, id) {
if (this.sessions.hasOwnProperty(id)) {
return;
}
this.add(id, s);
}, this));
console.log("Merged sessions", this, otherSession);
};
return {
create: function(data) {
return new BuddySession(data);
},
sessions: function() {
return sessions;
}
};
}];
});

103
static/js/services/contactdata.js

@ -0,0 +1,103 @@ @@ -0,0 +1,103 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['underscore', 'jquery'], function(underscore, $) {
// contactData
return [function() {
var contacts = {};
var users = {};
var count = 0;
var contactData = {
clear: function(cb) {
_.each(users, _.bind(function(idx, userid) {
var contact = contacts[idx];
if (cb && contact) {
cb(contact);
}
this.remove(userid);
}, this));
count = 0;
},
addByRequest: function(request, status) {
//console.log("addByRequest", request, status);
var userid = request.Userid;
var token = request.Token;
var id;
if (users.hasOwnProperty(userid)) {
// Existing contact. Replace it.
id = users[userid];
} else {
id = String(count++);
users[userid] = id;
}
var contact = contacts[id] = {
Id: "contact-"+id,
Userid: userid,
Token: token,
Status: null
}
return contact;
},
addByData: function(data) {
//console.log("addByData", data.Userid, data);
var userid = data.Userid;
if (users.hasOwnProperty(userid)) {
id = users[userid]
} else {
id = String(count++);
users[userid] = id;
}
var contact = contacts[id] = data;
contact.Id = id;
return contact;
},
get: function(userid) {
if (users.hasOwnProperty(userid)) {
var id = users[userid];
return contacts[id];
}
return null;
},
remove: function(userid) {
if (users.hasOwnProperty(userid)) {
var id = users[userid];
delete contacts[id];
}
delete users[userid];
},
getById: function(id) {
if (id.indexOf("contact-") === 0) {
id = id.substr(8)
}
if (contacts.hasOwnProperty(id)) {
return contacts[id];
}
return null
}
};
return contactData;
}];
});

265
static/js/services/contacts.js

@ -0,0 +1,265 @@ @@ -0,0 +1,265 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['underscore', 'jquery', 'modernizr', 'sjcl'], function(underscore, $, Modernizr, sjcl) {
var Database = function(name) {
this.version = 3;
this.ready = false;
this.db = null;
this.name = name;
this.e = $({});
var request = indexedDB.open(this.name, this.version);
var that = this;
request.onupgradeneeded = function(event) {
var db = event.target.result;
var transaction = event.target.transaction;
transaction.onerror = _.bind(that.onerror, that);
that.init(db);
console.log("Created contacts database.")
};
request.onsuccess = _.bind(that.onsuccess, that);
};
Database.prototype.init = function(db) {
var createOrUpdateStore = function(name, obj) {
if (db.objectStoreNames.contains(name)) {
// TODO(longsleep): Migrate data.
db.deleteObjectStore(name);
}
db.createObjectStore(name, obj);
}
// Create our object stores.
createOrUpdateStore("contacts", {
// We use id field as our unique identifier.
keyPath: "id"
});
};
Database.prototype.onerror = function(event) {
console.log("IndexDB database error", event);
};
Database.prototype.onsuccess = function(event) {
this.db = event.target.result;
this.ready = true;
console.log("Openend database", this.db);
this.e.triggerHandler("ready");
};
Database.prototype.put = function(storename, data, successCallback, errorCallback) {
var transaction = this.db.transaction(storename, "readwrite");
var store = transaction.objectStore(storename);
var request = store.put(data);
if (!errorCallback) {
errorCallback = _.bind(this.onerror, this);
}
transaction.onerror = request.onerror = errorCallback;
if (successCallback) {
request.onsuccess = successCallback;
}
return request;
};
Database.prototype.delete = function(storename, id, successCallback, errorCallback) {
var transaction = this.db.transaction(storename, "readwrite");
var store = transaction.objectStore(storename);
var request = store.delete(id);
if (!errorCallback) {
errorCallback = _.bind(this.onerror, this);
}
transaction.onerror = request.onerror = errorCallback;
if (successCallback) {
request.onsuccess = successCallback;
}
return request;
};
Database.prototype.all = function(storename, iteratorCallback, errorCallback) {
var transaction = this.db.transaction(storename);
var store = transaction.objectStore(storename);
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
cursorRequest.onsuccess = function(event) {
var result = event.target.result;
if (!result) {
return;
}
//console.log("read data idb", result, event);
iteratorCallback(result.value);
result.continue();
};
if (!errorCallback) {
errorCallback = _.bind(this.onerror, this);
}
transaction.onerror = cursorRequest.onerror = errorCallback;
return cursorRequest;
};
Database.prototype.close = function() {
// TODO(longsleep): Database close.
this.e.off();
if (this.db) {
this.db.close();
this.db = null;
this.ready = false;
}
_.defer(_.bind(function() {
this.e.triggerHandler("closed");
}, this));
};
// contacts
return ["appData", "contactData", "mediaStream", function(appData, contactData, mediaStream) {
var Contacts = function() {
this.e = $({});
this.userid = null;
this.key = null;
this.database = null;
appData.e.on("authenticationChanged", _.bind(function(event, userid, suserid) {
// TODO(longsleep): Avoid creating empty databases. Create db on store only.
var database = this.open(userid, suserid);
if (database && userid) {
// Load when database is ready and userid is available.
if (database.ready) {
_.defer(_.bind(function() {
if (this.database === database) {
this.load();
}
}, this));
} else {
database.e.one("ready", _.bind(function() {
if (this.database === database) {
this.load();
}
}, this));
}
}
}, this));
};
Contacts.prototype.open = function(userid, suserid) {
if (this.database && (!userid || this.userid !== userid)) {
// Unload existing contacts.
this.unload();
// Close existing database.
this.database.close();
this.database = null;
}
if (userid) {
if (!Modernizr.indexeddb) {
return;
}
// Create secure key for hashing and encryption.
this.key = sjcl.codec.base64.fromBits(sjcl.hash.sha256.hash(suserid+mediaStream.config.Token));
// Create database name for user which.
var id = "mediastream-" + this.id(userid);
console.log("Open of database:", id);
var database = this.database = new Database(id);
return database;
} else {
this.database = null;
return null;
}
};
Contacts.prototype.id = function(userid) {
var hmac = new sjcl.misc.hmac(this.key);
return sjcl.codec.base64.fromBits(hmac.encrypt(userid));
};
Contacts.prototype.encrypt = function(data) {
return sjcl.encrypt(this.key, JSON.stringify(data));
};
Contacts.prototype.decrypt = function(data) {
var result;
try {
var s = sjcl.decrypt(this.key, data);
result = JSON.parse(s);
} catch(err) {
console.error("Failed to decrypt contact data", err);
}
return result;
};
Contacts.prototype.load = function() {
if (this.database) {
console.log("Load contacts from storage", this);
var remove = [];
this.database.all("contacts", _.bind(function(data) {
var d = this.decrypt(data.contact);
if (d) {
var contact = contactData.addByData(d);
// TODO(longsleep): Convert buddyImage string to Blob.
this.e.triggerHandler("contactadded", d);
} else {
// Remove empty or invalid entries automatically.
remove.push(data.id);
}
}, this));
// Remove marked entries.
if (remove.length) {
_.each(remove, _.bind(function(id) {
this.database.delete("contacts", id);
}, this));
}
}
};
Contacts.prototype.unload = function() {
contactData.clear(_.bind(function(contact) {
this.e.triggerHandler("contactremoved", contact);
}, this));
};
Contacts.prototype.add = function(request, status) {
var contact = contactData.addByRequest(request, status);
this.e.triggerHandler("contactadded", contact);
if (this.database) {
this.database.put("contacts", {
id: this.id(contact.Userid),
contact: this.encrypt(contact)
})
}
};
Contacts.prototype.remove = function(userid) {
var contact = contactData.get(userid);
console.log("contacts remove", userid, contact);
if (contact) {
contactData.remove(userid);
if (this.database) {
this.database.delete("contacts", this.id(userid));
}
this.e.triggerHandler("contactremoved", contact);
}
};
return new Contacts();
}];
});

73
static/js/services/localstorage.js

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(["modernizr"], function(Modernizr) {
// localStorage
return ["$window", function($window) {
// PersistentStorage (c)2014 struktur AG. MIT license.
var PersistentStorage = function(prefix) {
this.prefix = prefix ? prefix : "ps";
this.isPersistentStorage = true;
};
PersistentStorage.prototype.setItem = function(key, data) {
var name = this.prefix+"_"+key;
$window.document.cookie = name + "=" + data + "; path=/";
};
PersistentStorage.prototype.getItem = function(key) {
var name = this.prefix+"_"+key+"=";
var ca = $window.document.cookie.split(';');
for (var i=0; i<ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return null;
};
PersistentStorage.prototype.removeItem = function(key) {
var name = this.prefix+"_"+key;
$window.document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
};
var storage;
if (Modernizr.localstorage) {
storage = $window.localStorage;
} else {
storage = new PersistentStorage();
}
// public API.
return {
setItem: function(key, data) {
return storage.setItem(key, data);
},
getItem: function(key) {
return storage.getItem(key);
},
removeItem: function(key) {
return storage.removeItem(key);
}
}
}];
});

2
static/js/services/mediastream.js

@ -29,7 +29,7 @@ define([ @@ -29,7 +29,7 @@ define([
], function($, _, uaparser, Connector, Api, WebRTC, tokens) {
return ["globalContext", "$route", "$location", "$window", "visibility", "alertify", "$http", "safeApply", "$timeout", "$sce", function(context, $route, $location, $window, visibility, alertify, $http, safeApply, $timeout, $sce) {
return ["globalContext", "$route", "$location", "$window", "visibility", "alertify", "$http", "safeApply", "$timeout", "$sce", "localStorage", function(context, $route, $location, $window, visibility, alertify, $http, safeApply, $timeout, $sce, localStorage) {
var url = (context.Ssl ? "wss" : "ws") + "://" + context.Host + (context.Cfg.B || "/") + "ws";
var version = context.Cfg.Version || "unknown";

21
static/js/services/services.js

@ -43,7 +43,12 @@ define([ @@ -43,7 +43,12 @@ define([
'services/randomgen',
'services/fastscroll',
'services/videowaiter',
'services/videolayout'], function(_,
'services/videolayout',
'services/contactdata',
'services/contacts',
'services/buddysession',
'services/localstorage',
'services/animationframe'], function(_,
desktopNotify,
playSound,
safeApply,
@ -66,7 +71,12 @@ safeDisplayName, @@ -66,7 +71,12 @@ safeDisplayName,
randomGen,
fastScroll,
videoWaiter,
videoLayout) {
videoLayout,
contactData,
contacts,
buddySession,
localStorage,
animationFrame) {
var services = {
desktopNotify: desktopNotify,
@ -91,7 +101,12 @@ videoLayout) { @@ -91,7 +101,12 @@ videoLayout) {
randomGen: randomGen,
fastScroll: fastScroll,
videoWaiter: videoWaiter,
videoLayout: videoLayout
videoLayout: videoLayout,
contactData: contactData,
contacts: contacts,
buddySession: buddySession,
localStorage: localStorage,
animationFrame: animationFrame
};
var initialize = function(angModule) {

6
static/partials/buddy.html

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<div class="buddy withSubline">
<div class="buddyPicture"><i class="fa fa-user fa-3x"/><img ng-show="status.buddyPicture" alt ng-src="{{status.buddyPicture}}" width="46" height="46"/></div>
<div class="buddy" ng-class="{'contact': contact, 'withSubline': display.subline || session.Userid}">
<div class="buddyPicture"><i class="fa fa-user fa-3x"/><img ng-show="display.buddyPicture" alt ng-src="{{display.buddyPicture}}" width="46" height="46"/></div>
<div class="buddy1">{{session.Id|displayName}}</div>
<div class="buddy2"><i ng-show="session.Userid" class="fa fa-star-o"></i> {{session.Ua}}</div>
<div class="buddy2"><span ng-show="session.Userid"><i class="fa contact" data-action="contact"></i><span ng-show="session.count"> ({{session.count}})</span></span> <span title="{{display.sublineFull}}">{{display.subline}}</span></div>
</div>

13
static/partials/buddyactions.html

@ -1,4 +1,11 @@ @@ -1,4 +1,11 @@
<div class="buddyactions active">
<a class="btn btn-info" title="{{_('Start video call')}}"><i class="fa fa-eye"></i></a>
<a class="btn btn-info" title="{{_('Start chat')}}" ng-click="doChat(session.Id); $event.stopPropagation()"><i class="fa fa-comments-o"></i></a>
<div class="buddyhover">
<div class="buddyactions active">
<a class="btn btn-info" data-action="call" title="{{_('Start video call')}}"><i class="fa fa-eye"></i></a>
<a class="btn btn-info" data-action="chat" title="{{_('Start chat')}}"><i class="fa fa-comments-o"></i></a>
</div>
<div class="buddysessions" ng-if="session.count>1">
<ul>
<li ng-repeat="(id, s) in session.sessions" ng-class="{currentsession: s.Id === session.Id}"><!--<div class="buddyPicture buddyPictureSmall"><i class="fa fa-user fa-2x"/><img ng-show="s.Status.buddyPicture" alt ng-src="{{s.Status.buddyPicture}}" width="30" height="30"/></div>--><span class="btn-group"><a class="btn btn-sm btn-default buddy3" ng-click="$event.stopPropagation()"><span>{{s.Status.displayName}}, {{s.Message}}</span></a><a class="btn btn-sm btn-default" title="{{_('Start video call')}}" ng-click="doCall(s.Id); $event.stopPropagation()"><i class="fa fa-eye"></i></a><a class="btn btn-sm btn-default" title="{{_('Start chat')}}" ng-click="doChat(s.Id); $event.stopPropagation()"><i class="fa fa-comments-o"></i></a></span></li>
</ul>
</div>
</div>

2
static/partials/buddylist.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<div class="buddylist nicescroll" ng-show="enabled" ng-class="{loading: loading, empty: empty}">
<div class="buddylist nicescroll" ng-class="{loading: loading, empty: empty}">
<div class="buddycontainer">
<div>
</div>

10
static/partials/contactrequest.html

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
<div class="contact-request">
<div ng-if="!fromself && request.Userid && !request.Success && request.Token">
<div ng-switch="state">
<div ng-switch-when="request">
<button ng-click="doAccept()" class="btn btn-success btn-sm">Accept</button>
<button ng-click="doReject()" class="btn btn-danger btn-sm">Reject</button>
</div>
</div>
</div>
</div>

2
static/partials/page/welcome.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<div class="welcome container" ng-controller="RoomchangeController">
<div class="welcome container-fluid" ng-controller="RoomchangeController">
<h3 style="margin-top:0px">{{_("Create your room")}}</h3>
<p><i>{{_("This is your room link:")}}</i></p>
<p>

115
static/partials/settings.html

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
<div class="form-horizontal" on-enter="saveSettings(user)" on-escape="reset()"
<fieldset>
<legend>{{_('Settings')}}</legend>
<legend>{{_('Profile')}}</legend>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Your picture')}}</label>
<div class="col-xs-8">
@ -16,65 +16,84 @@ @@ -16,65 +16,84 @@
<label class="col-xs-4 control-label">{{_('Your name')}}</label>
<div class="col-xs-8">
<input type="text" class="form-control" ng-model="user.displayName" placeholder="{{_('Name')}}" />
<span class="help-block">{{_('Your picture and name are visible to others.')}}</span>
</div>
</div>
<div class="form-group" ng-if="(withUsers && withUsersRegistration) || userid">
<label class="col-xs-4 control-label">{{_('Your ID')}}</label>
<div ng-switch="withUsersMode">
<form ng-switch-when="certificate" class="col-xs-8" target="users_registration_certificate_iframe">
<div ng-if="!userid">
<keygen style="display:none" name="pubkey"/>
<label ng-if="!userid && withUsersRegistration">
<button class="btn btn-small btn-primary" ng-click="registerUserid($event.target)">{{_('Register')}}</button>
</label>
<iframe style="display:none" name="users_registration_certificate_iframe"></iframe>
</div>
<div ng-if="userid">
<pre class="small">{{userid}}</pre>
<span class-"help-block">{{_('Authenticated by certificate. To log out you have to remove your certificate from the browser.')}}</span>
</div>
</form>
<div ng-switch-default class="col-xs-8">
<label ng-if="!userid && withUsersRegistration">
<button class="btn btn-small btn-primary" ng-click="registerUserid($event.target)">{{_('Register')}}</button>
</label>
<pre class="small" ng-if="userid">{{userid}}</pre>
<label ng-if="userid && loadedUserlogin">
<button class="btn btn-small btn-default" ng-click="forgetUserid()">{{_('Log out')}}</button>
</label>
</div>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Status message')}}</label>
<div class="col-xs-8">
<input type="text" class="form-control" ng-model="user.message" placeholder="{{_('What\'s on your mind?')}}" />
</div>
<div class="col-xs-8 col-xs-offset-4" ng-if="!userid && withUsersRegistration">
<span class="help-block">{{_('Only register an ID if this is your private browser.')}}</span>
</div>
<div class="form-group">
<div class="col-xs-8 col-xs-offset-4">
{{_('Profile information is public.')}}
</div>
</div>
<div ng-show="mediaSources.supported">
<hr/>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Microphone')}}</label>
<div class="col-xs-8">
<select class="form-control" ng-model="user.settings.microphoneId" ng-options="mic.id as mic.label for mic in mediaSources.audio"></select>
<div ng-controller="UsersettingsController as usersettings">
<div ng-if="(withUsers && withUsersRegistration) || userid">
<legend>{{_('Account')}}</legend>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Your ID')}}</label>
<div ng-switch="withUsersMode">
<form ng-switch-when="certificate" class="col-xs-8" target="users_registration_certificate_iframe">
<div ng-if="!userid">
<keygen style="display:none" name="pubkey"/>
<label ng-if="!userid && withUsersRegistration && usersettings.registerUserid">
<button class="btn btn-small btn-primary" ng-click="usersettings.registerUserid($event.target)">{{_('Register')}}</button>
</label>
<iframe style="display:none" name="users_registration_certificate_iframe"></iframe>
</div>
<div ng-if="userid">
<pre class="small">{{userid}}</pre>
<span class-"help-block">{{_('Authenticated by certificate. To log out you have to remove your certificate from the browser.')}}</span>
</div>
</form>
<div ng-switch-default class="col-xs-8">
<div ng-if="!userid && withUsersRegistration">
<label ng-if="usersettings.loginUserid">
<button class="btn btn-small btn-primary" ng-click="usersettings.loginUserid()">{{_('Login')}}</button>
</label>
<label ng-if="usersettings.registerUserid">
<button class="btn btn-small btn-default" ng-click="usersettings.registerUserid($event.target)">{{_('Register')}}</button>
</label>
</div>
<pre class="small" ng-if="userid">{{userid}}</pre>
<label ng-if="userid && withUsersForget && usersettings.forgetUserid">
<button class="btn btn-small btn-default" ng-click="usersettings.forgetUserid()">{{_('Log out')}}</button>
</label>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Camera')}}</label>
<div class="col-xs-8">
<select class="form-control" ng-model="user.settings.cameraId" ng-options="vid.id as vid.label for vid in mediaSources.video"></select>
</div>
<div ng-show="mediaSources.supported || isChrome">
<legend>{{_('Media')}}</legend>
<div ng-show="mediaSources.supported">
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Microphone')}}</label>
<div class="col-xs-8">
<select class="form-control" ng-model="user.settings.microphoneId" ng-options="mic.id as mic.label for mic in mediaSources.audio"></select>
</div>
</div>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Camera')}}</label>
<div class="col-xs-8">
<select class="form-control" ng-model="user.settings.cameraId" ng-options="vid.id as vid.label for vid in mediaSources.video"></select>
</div>
</div>
</div>
</div>
<div ng-show="isChrome" class="form-group">
<label class="col-xs-4 control-label">{{_('Video quality')}}</label>
<div class="col-xs-8">
<div class="btn-group">
<button type="button" class="btn btn-default" ng-model="user.settings.videoQuality" btn-radio="'low'">{{_('Low')}}</button>
<button type="button" class="btn btn-default" ng-model="user.settings.videoQuality" btn-radio="'high'">{{_('High')}}</button>
<button type="button" class="btn btn-default" ng-model="user.settings.videoQuality" btn-radio="'hd'">{{_('HD')}}</button>
<div ng-show="isChrome" class="form-group">
<label class="col-xs-4 control-label">{{_('Video quality')}}</label>
<div class="col-xs-8">
<div class="btn-group">
<button type="button" class="btn btn-default" ng-model="user.settings.videoQuality" btn-radio="'low'">{{_('Low')}}</button>
<button type="button" class="btn btn-default" ng-model="user.settings.videoQuality" btn-radio="'high'">{{_('High')}}</button>
<button type="button" class="btn btn-default" ng-model="user.settings.videoQuality" btn-radio="'hd'">{{_('HD')}}</button>
</div>
</div>
</div>
</div>
<hr/>
<legend>{{_('Settings')}}</legend>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Language')}}</label>
<div class="col-xs-8">

Loading…
Cancel
Save