Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'master' of https://github.com/buddycloud/buddycloud-web…

  • Loading branch information...
commit 71dbb6a907cf89ef2173dab029a0213b59c8b68b 2 parents 8c1f126 + a778cf4
@mrflix mrflix authored
Showing with 2,009 additions and 631 deletions.
  1. +11 −7 assets/config.js
  2. +18 −5 assets/create_topic_channel.html
  3. +3 −1 assets/index.html
  4. +31 −19 assets/streams.html
  5. +0 −1  brunch/src/vendor/dynamictemplate.js
  6. +9 −7 package.json
  7. +0 −2  readme.md
  8. +35 −60 src/build/packaging.coffee
  9. +12 −5 src/build/server.coffee
  10. +0 −15 src/build/util.coffee
  11. +3 −0  src/collections/base.coffee
  12. +12 −9 src/collections/channel.coffee
  13. +4 −0 src/controllers/router.coffee
  14. +2 −0  src/handlers/connection.coffee
  15. +71 −12 src/handlers/connector.coffee
  16. +146 −88 src/handlers/data.coffee
  17. +38 −0 src/handlers/rsm_queue.coffee
  18. +67 −48 src/init.coffee
  19. +21 −2 src/models/channel.coffee
  20. +0 −4 src/models/metadata/node.coffee
  21. +24 −50 src/models/node/base.coffee
  22. +70 −1 src/models/user.coffee
  23. +15 −0 src/styles/channels.styl
  24. +1 −0  src/styles/edit.styl
  25. +1 −1  src/styles/forms.styl
  26. +10 −1 src/styles/main.styl
  27. +20 −7 src/styles/stream.styl
  28. +8 −1 src/templates/channel/comments.coffee
  29. +35 −10 src/templates/channel/details/index.coffee
  30. +27 −38 src/templates/channel/details/list.coffee
  31. +157 −0 src/templates/channel/details/user.coffee
  32. +15 −5 src/templates/channel/edit.coffee
  33. +25 −0 src/templates/channel/error_notification.coffee
  34. +46 −0 src/templates/channel/follow_notification.coffee
  35. +28 −8 src/templates/channel/index.coffee
  36. +27 −0 src/templates/channel/pending_notification.coffee
  37. +55 −33 src/templates/channel/post.coffee
  38. +14 −0 src/templates/channel/private.coffee
  39. +52 −0 src/templates/create_topic_channel/index.coffee
  40. +20 −3 src/templates/sidebar/entry.coffee
  41. +4 −0 src/templates/sidebar/index.coffee
  42. +57 −20 src/util.coffee
  43. +7 −1 src/vendor/backbone-extensions.js
  44. +3 −3 src/vendor/modernizr.js
  45. +52 −7 src/vendor/strophe.buddycloud.js
  46. +67 −8 src/vendor/strophe.pubsub.js
  47. +8 −5 src/views/authentication/base.coffee
  48. +2 −2 src/views/authentication/login.coffee
  49. +13 −0 src/views/authentication/overlay.coffee
  50. +3 −1 src/views/base.coffee
  51. +20 −8 src/views/channel/details/index.coffee
  52. +75 −4 src/views/channel/details/list.coffee
  53. +76 −0 src/views/channel/details/user.coffee
  54. +164 −54 src/views/channel/edit.coffee
  55. +16 −3 src/views/channel/error_notification.coffee
  56. +42 −0 src/views/channel/follow_notification.coffee
  57. +138 −64 src/views/channel/index.coffee
  58. +16 −0 src/views/channel/pending_notification.coffee
  59. +32 −2 src/views/channel/post.coffee
  60. +12 −6 src/views/channel/posts.coffee
  61. +33 −0 src/views/create_topic_channel/index.coffee
  62. +7 −0 src/views/main.coffee
  63. +9 −0 src/views/sidebar/entry.coffee
  64. +11 −0 src/views/sidebar/index.coffee
  65. +9 −0 src/views/sidebar/search.coffee
View
18 assets/config.js
@@ -3,18 +3,22 @@
window.config = {
/* address of the bosh gateway. this should be reachable from webclient domain */
- bosh_service: 'http://beta.buddycloud.org:5280/http-bind/',
- //bosh_service: 'https://beta.buddycloud.org:443/http-bind/', // secure
- //bosh_service: 'http://bosh.metajack.im:5280/xmpp-httpbind', // just for testing!
+ bosh_service: 'https://beta.buddycloud.org:443/http-bind/',
/*this is the inbox domain for anon users */
- home_domain: "buddycloud.org",
+ home_domain: "example.com",
/* domain to authenticate against for anon users */
- anon_domain: "anon.buddycloud.org",
+ anon_domain: "anon.example.com",
/* overall used domain for this webclient instance.
* used for registration and login. */
- domain: "buddycloud.org",
- embedly_key: "2c1bedbc2aa111e1acbf4040d3dc5c07"
+ domain: "example.com",
+
+ /* Default domain to create topics under
+ * unless the user specifies …@domain as the name. */
+ //topic_domain: "topics.example.com",
+
+ /* Sign up for an embed.ly account to use OStatus */
+ //embedly_key: "xxx"
};
View
23 assets/create_topic_channel.html
@@ -225,17 +225,17 @@ <h2 class="title">Create Channel</h2>
<div class="access">
<label for="channel_public_access">Public Access</label>
<div>
- <input type="checkbox" id="channel_public_access"/>
+ <input type="checkbox" id="channel_public_access" checked="checked"/>
</div>
</div>
<div class="role">
<label for="channel_default_role">Default Role</label>
<div>
<select id="channel_default_role">
- <option value="follower">follower</option>
- <option value="followerPlus">follower+</option>
+ <option value="member">follower</option>
+ <option value="publisher" selected>follower+post</option>
</select>
- <span class="hint followerSelected">
+ <span class="hint followerPlusSelected">
<span class="follower">
can only read your channel
</span>
@@ -245,9 +245,22 @@ <h2 class="title">Create Channel</h2>
</span>
</div>
</div>
+ <div class="publish">
+ <label for="channel_publish">Posting</label>
+ <div>
+ <select id="channel_publish">
+ <option value="open">Anyone</option>
+ <option value="subscribers" selected="selected">Followers</option>
+ <option value="moderators">Moderators</option>
+ </select>
+ <span class="hint followerSelected">
+ can post
+ </span>
+ </div>
+ </div>
</form>
<nav class="bottom clearfix">
- <div class="button callToAction">Create</div>
+ <div id="create_button" class="button callToAction">Create</div>
<div class="button">Discard</div>
</nav>
</div><!-- /channelView -->
View
4 assets/index.html
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="web/css/main.css" type="text/css" media="screen">
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript" src="web/js/app.js"></script>
- <script type="text/javascript">require('./init');</script>
+ <link rel="icon" href="favicon.ico">
</head>
<body class="start">
@@ -27,6 +27,7 @@
<h2>buddycloud login</h2>
<div class="error">
<div id="nobosh">BOSH Service unavailable!</div>
+ <div id="nochannelserver">Channel Server unreachable!</div>
<div id="authfail">Unable to confirm your username or password. Please try again.</div>
<div id="connfail">Connection to the server closed.</div>
<div id="disconnected">Thats weird. You disconnected.</div>
@@ -58,6 +59,7 @@
<p class="negative">Let's get setup</p>
<div class="error">
<div id="nobosh">BOSH Service unavailable!</div>
+ <div id="nochannelserver">Channel Server unreachable!</div>
<div id="regifail">Cannot create new Account.</div>
<div id="authfail">Unable to confirm your username or password.</div>
<div id="connfail">Connection to the server closed.</div>
View
50 assets/streams.html
@@ -20,13 +20,21 @@
-->
<div class="edits">
<div class="contenteditable">
- <input id="allowPost" type="checkbox">
- <label for="allowPost">New followers can post</label>
+ <input id="allowPost" type="checkbox">
+ <label for="allowPost">New followers can post</label>
</div>
- <div class="contenteditable">
+ <!--div class="contenteditable">
+ <select id="channel_publish">
+ <option value="open">Anyone</option>
+ <option value="subscribers">Followers</option>
+ <option value="moderators">Moderators</option>
+ </select>
+ <label for="channel_publish">can post</label>
+ </div-->
+ <!--div class="contenteditable">
<label for="userEmail">Email</label>
<input id="userEmail" type="email" value="vera@buddycloud.com" placeholder="Email Adress">
- </div>
+ </div-->
<nav class="clearfix">
<div class="button translucid cancel">Cancel</div>
<div class="button prominent save">Save</div>
@@ -211,6 +219,7 @@
</div>
</div><!-- /antiscroll-inner -->
</div><!-- /channels -->
+ <p id="create_topic_channel">Create topic channel…</p>
<!-- replaced by scrolling
<div id="more_channels">
all Channels
@@ -249,20 +258,23 @@ <h2 class="title" data-editmode="singleLine">Vera Meisterkoch's World</h2>
<div class="button prominent" id="createNewTopic">Post</div>
</div>
</section>
- <!-- this produces some weird bug - some content below it gets lost when this is visible
- <article class="notification">
- <section>
- <img class="avatar" src="public/avatars/user4.jpg" />
- <span class="name">Martin</span>
- <p>wants to follow this channel.</p>
- <span class="granted">started following this channel.</span>
- <span class="denied">was denied to follow this channel.</span>
- <div class="controls">
- <div class="button small negative light">Deny</div>
- <div class="button small prominent positive light">Grant Martin to join</div>
- </div>
- </section>
- </article>-->
+ <!-- this produces some weird bug - some content below
+ it gets lost when this is visible -->
+ <div class="notifications">
+ <article class="notification">
+ <section>
+ <img class="avatar" src="public/avatars/user4.jpg" />
+ <span class="name">Martin</span>
+ <p>wants to follow this channel.</p>
+ <span class="granted">started following this channel.</span>
+ <span class="denied">was denied to follow this channel.</span>
+ <div class="controls">
+ <div class="button small negative light">Deny</div>
+ <div class="button small positive light">Grant Martin to join</div>
+ </div>
+ </section>
+ </article>
+ </div>
<section class="topics">
<article class="topic">
<section class="opener">
@@ -506,7 +518,7 @@ <h4 class="title">Info</h4>
</ul>
</div>
</section>
- <section class="actionRow">
+ <section class="actionRow choose">
<div>Change Role</div>
<div>Ban User</div>
</section>
View
1  brunch/src/vendor/dynamictemplate.js
View
16 package.json
@@ -1,6 +1,6 @@
{ "name": "channel-webclient"
, "description": "buddycloud webclient"
-, "version": "0.0.0-47"
+, "version": "0.0.0-57"
, "author": "?"
, "homepage": "https://github.com/buddycloud/channel-webclient"
, "repository": { "type": "git", "url": "git://github.com/buddycloud/channel-webclient.git" }
@@ -12,9 +12,9 @@
, "dependencies": {
"coffee-script": "1.1.3"
, "colors": "0.6.0-1"
-, "tar": "0.1.9"
-, "zlib": "1.0.5"
-, "bufferstream": "0.5.0-pre"
+, "tarball": "0.0.3"
+, "request": "2.9.100"
+, "bufferstream": "0.5.0"
, "node-dev": "0.1.9"
, "express": "2.5.2"
, "jsconfig": "0.1.2"
@@ -24,9 +24,10 @@
, "shimify": "0.0.0"
, "jqueryify": "0.0.2"
, "underscore": "1.3.0"
+, "scopify": "0.2.1"
, "browserify": "https://github.com/poelzi/node-browserify/tarball/master"
, "backbone-browserify": "0.5.3"
-, "jquery-browserify": "1.6.2"
+, "br-jquery": "0.0.1"
, "jquery-autosuggestion": "0.0.3"
, "jquery-inputevent": "0.1.4"
, "jquery-textsaver": "0.1.4"
@@ -34,9 +35,10 @@
, "store": "1.1.1"
, "async": "0.1.15"
, "dynamictemplate": "0.4.2"
-, "dt-compiler": "0.1.1"
-, "dt-jquery": "0.2.3"
+, "dt-compiler": "0.1.5"
+, "dt-jquery": "0.2.6"
, "Strophe.js": "https://github.com/metajack/strophejs/tarball/master"
+, "notificon": "https://github.com/dodo/Notificon/tarball/master"
}
, "_dependencies_missing_from_npm": {
"jQuery.fn.autoResize": "5025b4a5c3ed60d7218827eb19e8c0c4e641aabe",
View
2  readme.md
@@ -23,7 +23,5 @@ This code is Apache 2 licensed and copyright buddycloud.
# Attribution
* lock/unlock icons: By Yusuke Kamiyamane, licensed under Creative Commons Attribution 3.0 license. "unlock-small.png" is a version of "lock-small.png" modified by mrflix.
-* other iconography from "Free Wireframe Toolbar Icons" by Gentleface, licensed under a The Creative Commons Attribution-NonCommercial use license. *http://gentleface.com/free_icon_set.html)
-
[![Build Status](https://secure.travis-ci.org/buddycloud/buddycloud-webclient.png)](http://travis-ci.org/buddycloud/buddycloud-webclient)
View
95 src/build/packaging.coffee
@@ -1,11 +1,10 @@
-tar = require 'tar'
-zlib = require 'zlib'
-http = require 'http'
-url = require 'url'
-config = require 'jsconfig'
-PostBuffer = require 'bufferstream/postbuffer'
-{ spiderDir, BufferedStream } = require './util'
{ createWriteStream } = require 'fs'
+request = require 'request'
+async = require 'async'
+config = require 'jsconfig'
+BufferStream = require 'bufferstream'
+{ Pack:Tarball } = require 'tarball'
+{ spiderDir } = require './util'
onError = (e) ->
console.error "#{e.stack or e.message or e}".red
@@ -13,8 +12,9 @@ onError = (e) ->
entries = [
- ""
+ "index.html"
"config.js"
+ "favicon.ico"
"web/js/app.js"
"web/js/store.js"
"web/css/main.css"
@@ -22,11 +22,16 @@ entries = [
-module.exports = (baseUrl, tarPath) ->
- tarPack = new tar.Pack(noProprietary: yes)
- tarPack
- .on('error', onError)
- .pipe(zlib.Gzip())
+module.exports = (tarPath) ->
+ tarball = new Tarball {noProprietary:yes},
+ compress:on
+ defaults:
+ uname:'www'
+ gname:'nogroup'
+ uid: 1000
+ gid: 1000
+
+ tarball
.on('error', onError)
.pipe(createWriteStream(tarPath))
.on('error', onError)
@@ -34,53 +39,23 @@ module.exports = (baseUrl, tarPath) ->
console.log "Built #{tarPath}".bold.green
process.exit 0
- idle = yes
-
- pushNextEntry = ->
- return unless idle
- idle = no
-
- entry = entries.shift()
- unless entry?
- return tarPack.end()
-
-
+ # using mapSeries because we dont to glutter the terminal
+ async.mapSeries( entries
+ ,(entry, done) ->
msg = "GET".cyan+" "+"/#{entry}".magenta+" "+"".bold.black
process.stdout.write msg
-
- u = url.parse("#{baseUrl}/#{entry}")
- req = http.get
- host: u.hostname
- port: u.port
- path: u.path
- path = if u.path == "/" then "/index.html" else u.path
- path = path.replace /^\/+/, ""
- req.on 'response', (res) ->
- # No Content-Length means we cannot pipe(). tar.Pack
- # needs to know a file's size beforehand though, so we
- # need to buffer the HTTP body.
- new PostBuffer(res).onEnd (body) ->
- console.log "","#{body?.length}".green,".".bold.black
- stream = new BufferedStream
- stream.props =
- path: path
- mode: 0755
- size: body.length
- uid: 1000
- gid: 1000
- uname: 'www'
- gname: 'nogroup'
- stream.root = path: "."
- stream.path = path
- flushed = tarPack.add stream
- stream.run body
-
- idle = yes
- if flushed
- process.nextTick pushNextEntry
- # else - Waiting for data to be flushed, continue
- req.on 'error', onError
-
- tarPack.on 'drain', pushNextEntry
- pushNextEntry()
+ request("http://#{config.host}:#{config.port}/#{entry}")
+ .on('error', onError)
+ .on 'response', (res) ->
+ stream = new BufferStream disabled:yes # no splitting needed
+ stream.path = entry # res.path can't be set
+ stream.props = size:res.headers['content-length']
+ tarball.append stream, ->
+ console.log "","#{stream.props.size}".green,".".bold.black
+ done()
+ res.pipe(stream)
+ ,(err) ->
+ onError(err) if err?
+ tarball.end()
+ )
View
17 src/build/server.coffee
@@ -13,9 +13,12 @@ browserify = require 'browserify'
snippets = ["main"
"channel/index", "channel/posts", "channel/post"
"channel/topicpost", "channel/comments", "channel/edit"
- "channel/details/index", "channel/details/list"
+ "channel/details/index", "channel/details/list", "channel/details/user"
+ "channel/follow_notification", "channel/pending_notification"
+ "channel/error_notification", "channel/private"
"sidebar/index", "sidebar/search", "sidebar/entry"
"authentication/overlay"
+ "create_topic_channel/index"
]
@@ -59,7 +62,8 @@ config.load (args, opts) ->
select: selector.select
watch: yes
done: done
- dest: path.join(designPath, selector.snippet) + ".js"
+ path: designPath
+ dest: "#{selector.snippet}.js"
0
start_server = (args, opts) ->
@@ -67,6 +71,7 @@ start_server = (args, opts) ->
server = express.createServer()
server.configure ->
+ server.use express.favicon(path.join(buildPath, "favicon.ico"))
javascript = browserify
mount : '/web/js/app.js'
@@ -75,7 +80,7 @@ start_server = (args, opts) ->
cache : on
debug : not config.build
require: [
- jquery :'jquery-browserify'
+ jquery :'br-jquery'
backbone:'backbone-browserify'
path.join(cwd, "src", "init")
]
@@ -93,6 +98,9 @@ start_server = (args, opts) ->
source += ";window.MD5=MD5"
source
+ javascript.use(require('shimify'))
+ javascript.use(require('scopify').createScope require:'./init')
+
if config.build
# minification
javascript.register 'post', require 'uglify-js'
@@ -136,8 +144,7 @@ start_server = (args, opts) ->
if config.build
# this puts everything in a tarball
pack = require './packaging'
- url = "http://#{config.host}:#{config.port}"
- pack url, "build.tar.gz"
+ pack "build.tar.gz"
else
console.log "build server listening on %s:%s …".magenta,
config.host, config.port
View
15 src/build/util.coffee
@@ -1,5 +1,4 @@
fs = require 'fs'
-{ Stream } = require 'stream'
spiderDir = (root, path) ->
@@ -15,19 +14,6 @@ spiderDir = (root, path) ->
results
-## this is just need to please tar
-# Emit a whole document at once
-class BufferedStream extends Stream
- run: (body) ->
- @emit 'data', body
- process.nextTick =>
- @emit 'end'
-
- resume: -> # stub
- pause: -> # stub
-
-
-
wrap_prefix = (prefix, middleware) ->
return (req, res, next) ->
if req.url.indexOf(prefix) is 0
@@ -43,7 +29,6 @@ wrap_prefix = (prefix, middleware) ->
# exports
module.exports = {
- BufferedStream
spiderDir
wrap_prefix
}
View
3  src/collections/base.coffee
@@ -1,5 +1,8 @@
+{ Model } = require '../models/base'
class exports.Collection extends Backbone.Collection
+ model: Model
+
constructor: (options) ->
@parent ?= options?.parent
super()
View
21 src/collections/channel.coffee
@@ -28,15 +28,18 @@ class exports.Channels extends Collection
filter: (filter) ->
return @models if not filter? or filter is ""
- filter = filter.toLowerCase()
- super (channel) ->
- # FIXME only id to check, there meight be more (hope so)
- if (id = channel.get('id'))?
- id.toLowerCase().indexOf(filter) > -1
-
- else
- # nothing to compare with, channel must be empty, so we can ignore it
- no
+ if typeof filter is 'string'
+ filter = filter.toLowerCase()
+ super (channel) ->
+ # FIXME only id to check, there might be more (hope so)
+ if (id = channel.get('id'))?
+ id.toLowerCase().indexOf(filter) > -1
+
+ else
+ # nothing to compare with, channel must be empty, so we can ignore it
+ no
+ else
+ super
touch: (channel, opts = {}) =>
channel.last_touched = opts.date or new Date
View
4 src/controllers/router.coffee
@@ -22,6 +22,10 @@ class exports.Router extends Backbone.Router
Backbone.history.start pushState:on
+ navigate: ->
+ # Avoid navigating while edit mode is on
+ unless app.views?.index?.current?.isEditing?()
+ super
setView: (view) ->
return unless view? # TODO access denied msg
View
2  src/handlers/connection.coffee
@@ -29,6 +29,7 @@ class exports.ConnectionHandler extends Backbone.EventHandler
# workaround for development
if window.location.hostname is "localhost"
+ # get around Access-Control-Allow-Origin restrictions
return callback()
# check if the bosh service is reachable
@@ -87,6 +88,7 @@ class exports.ConnectionHandler extends Backbone.EventHandler
@createChannel done
error = =>
app.error "discover_channel_server error", arguments
+ @trigger 'nochannelserver'
#done()
domain = if @user.get('jid') is "anony@mous"
View
83 src/handlers/connector.coffee
@@ -21,6 +21,17 @@ class exports.Connector extends Backbone.EventHandler
, (error) =>
callback? new Error("Cannot replay notifications")
+ createNode: (nodeid, metadata, callback) =>
+ @request (done) =>
+ success = =>
+ done()
+ callback?()
+ error = (error) =>
+ done()
+ callback?(error)
+ @connection.buddycloud.createNode(
+ nodeid, metadata, success, error)
+
publish: (nodeid, item, callback) =>
@request (done) =>
@connection.buddycloud.publishAtom nodeid, item
@@ -38,16 +49,15 @@ class exports.Connector extends Backbone.EventHandler
subscribe: (nodeid, callback) =>
@request (done) =>
# TODO: subscribe channel
- @connection.buddycloud.subscribeNode nodeid, (stanza) =>
- app.debug "subscribe", stanza
+ @connection.buddycloud.subscribeNode nodeid, (subscription) =>
userJid = Strophe.getBareJidFromJid(@connection.jid)
@trigger 'subscription',
jid: userJid
node: nodeid
- subscription: 'subscribed' # FIXME
+ subscription: subscription
@work_enqueue ->
done()
- callback? null
+ callback? null, subscription
, =>
app.error "subscribe", nodeid
@work_enqueue ->
@@ -62,7 +72,7 @@ class exports.Connector extends Backbone.EventHandler
@trigger 'subscription',
jid: userJid
node: nodeid
- subscription: 'unsubscribed'
+ subscription: 'none'
@work_enqueue ->
done()
callback? null
@@ -80,7 +90,7 @@ class exports.Connector extends Backbone.EventHandler
# app.error "fetch_node_posts", nodeid, arguments
# @connection.buddycloud.getChannelPostStream nodeid, success, error
- get_node_posts: (nodeid, rsmAfter, callback) =>
+ get_node_posts: ({nodeid, rsmAfter, itemIds}, callback) =>
@request (done) =>
success = (posts) =>
for post in posts
@@ -89,8 +99,6 @@ class exports.Connector extends Backbone.EventHandler
else if post.subscriptions?
for own nodeid_, subscription of post.subscriptions
@trigger 'subscription', subscription
- if posts.rsm
- @trigger 'posts:rsm:last', nodeid, posts.rsm.last
@work_enqueue ->
done()
callback? null, posts
@@ -100,8 +108,12 @@ class exports.Connector extends Backbone.EventHandler
@work_enqueue ->
done()
callback? new Error("Cannot get posts")
+ # Only most recent status is interesting for the status
+ # node. TODO: this amount parameter is better moved up
+ # towards the views that actually display it.
+ rsmMax = if /\/status/.test(nodeid) then 1 else 30
@connection.buddycloud.getChannelPosts(
- { node: nodeid, rsmAfter }, success, error, @connection.timeout)
+ { node: nodeid, rsmMax, rsmAfter, itemIds }, success, error, @connection.timeout)
get_node_metadata: (nodeid, callback) =>
@request (done) =>
@@ -129,11 +141,9 @@ class exports.Connector extends Backbone.EventHandler
jid: user
node: nodeid
subscription: subscription
- if subscribers.rsm
- @trigger 'subscribers:rsm:last', nodeid, subscribers.rsm.last
@work_enqueue ->
done()
- callback? null
+ callback? null, subscribers
error = (error) =>
@trigger 'node:error', nodeid, error
@work_enqueue ->
@@ -142,6 +152,55 @@ class exports.Connector extends Backbone.EventHandler
@connection.buddycloud.getSubscribers(
{ node: nodeid, rsmAfter }, success, error, @connection.timeout)
+ set_node_subscriptions: (nodeid, subscriptions, callback) =>
+ @request (done) =>
+ success = (affiliations) =>
+ @work_enqueue ->
+ done()
+ callback? null
+ error = (error) =>
+ @trigger 'node:error', nodeid, error
+ @work_enqueue ->
+ done()
+ callback? error
+ @connection.pubsub.setNodeSubscriptions(nodeid, subscriptions
+ , success, error, @connection.timeout)
+
+
+ get_node_affiliations: (nodeid, rsmAfter, callback) =>
+ @request (done) =>
+ success = (affiliations) =>
+ console.warn "affiliations", nodeid, affiliations
+ for own user, affiliation of affiliations
+ unless user is 'rsm'
+ @trigger 'affiliation',
+ jid: user
+ node: nodeid
+ affiliation: affiliation
+ @work_enqueue ->
+ done()
+ callback? null, affiliations
+ error = (error) =>
+ @trigger 'node:error', nodeid, error
+ @work_enqueue ->
+ done()
+ callback? new Error("Cannot get affiliations")
+ @connection.buddycloud.getAffiliations(
+ { node: nodeid, rsmAfter }, success, error, @connection.timeout)
+
+ set_node_affiliation: (nodeid, userid, affiliation, callback) =>
+ @request (done) =>
+ success = (affiliations) =>
+ @work_enqueue ->
+ done()
+ callback? null
+ error = (error) =>
+ @trigger 'node:error', nodeid, error
+ @work_enqueue ->
+ done()
+ callback? new Error("Cannot set affiliation")
+ @connection.pubsub.setAffiliation(nodeid, userid, affiliation, success, error)
+
set_node_metadata: (nodeid, metadata, callback) =>
@request (done) =>
success = =>
View
234 src/handlers/data.coffee
@@ -1,4 +1,6 @@
{ User } = require '../models/user'
+{ RSMQueue } = require './rsm_queue'
+async = require 'async'
class exports.DataHandler extends Backbone.EventHandler
@@ -15,30 +17,44 @@ class exports.DataHandler extends Backbone.EventHandler
@connector.bind 'connection:established', @on_connection_established
@connector.bind 'connection:end', @on_connection_end
- # TODO: @param node {Node model}
+ @get_posts_queue = new RSMQueue 'posts', (nodeid, rsmAfter, callback) =>
+ @connector.get_node_posts { nodeid, rsmAfter }, callback
+ @get_subscriptions_queue = new RSMQueue 'subscriptions', (nodeid, rsmAfter, callback) =>
+ @connector.get_node_subscriptions nodeid, rsmAfter, callback
+ @get_affiliations_queue = new RSMQueue 'affiliations', (nodeid, rsmAfter, callback) =>
+ @connector.get_node_affiliations nodeid, rsmAfter, callback
+
+ ##
+ # Extracts and sanitizes userid part from title, then creates
+ # posts & status nodes.
+ create_topic_channel: (metadata, callback) ->
+ userid = metadata.title.
+ toLocaleLowerCase().
+ replace(/\s/g, "_").
+ replace(/[\"\&\'\/\:\<\>]/g, "")
+ if userid.indexOf("@") < 0 and config.topic_domain
+ userid = "#{userid}@#{config.topic_domain}"
+ @connector.createNode "/user/#{userid}/posts", metadata, (err) =>
+ if err
+ return callback(err)
+
+ @connector.createNode "/user/#{userid}/status", metadata, ->
+ # Don't care about status node result if posts worked
+ callback(null, userid)
+
+ # @param node {Node model or nodeid}
+ # @param callback(err, done)
get_node_posts: (node, callback) ->
- nodeid = node.get?('nodeid') or node
if typeof node is 'string'
- channel = app.channels.get_or_create id:nodeid
- node = channel.nodes.get_or_create nodeid:nodeid
-
- # Reset pagination
- node.push_posts_rsm_last null
+ channel = app.channels.get_or_create id:node
+ node = channel.nodes.get_or_create nodeid:node
- @connector.get_node_posts nodeid, null, (err, posts) =>
- unless err
- # Success retrieving first page?
- node.on_posts_synced()
- callback? err, posts
+ @get_posts_queue.add node, callback
- get_more_node_posts: (node, callback) ->
+ get_node_posts_by_id: (node, ids, callback) ->
nodeid = node.get?('nodeid') or node
- if typeof node is 'string'
- channel = app.channels.get_or_create id:nodeid
- node = channel.nodes.get_or_create nodeid:nodeid
-
- rsm_after = node.posts_rsm_last
- @connector.get_node_posts nodeid, rsm_after, callback
+ console.warn "get_node_posts_by_id", node, ids
+ @connector.get_node_posts { nodeid, itemIds: ids }, callback
get_node_metadata: (node, callback) ->
nodeid = node.get?('nodeid') or node
@@ -49,28 +65,37 @@ class exports.DataHandler extends Backbone.EventHandler
@connector.set_node_metadata nodeid, metadata, callback
get_node_subscriptions: (node, callback) ->
- nodeid = node.get?('nodeid') or node
if typeof node is 'string'
- channel = app.channels.get_or_create id:nodeid
- node = channel.nodes.get_or_create nodeid:nodeid
+ channel = app.channels.get_or_create id:node
+ node = channel.nodes.get_or_create nodeid:node
- # Reset pagination
- node.push_subscribers_rsm_last null
+ @get_subscriptions_queue.add node, callback
- @connector.get_node_subscriptions nodeid, null, (err, subscribers) =>
- unless err
- # Success retrieving first page?
- node.on_subscribers_synced()
- callback? err, subscribers
+ get_all_node_subscriptions: (nodeid, callback) =>
+ @get_node_subscriptions nodeid, (err, results, done) =>
+ if err or done
+ callback?()
+ else
+ @get_all_node_subscriptions nodeid, callback
- get_more_node_subscriptions: (node, callback) ->
- nodeid = node.get?('nodeid') or node
+ get_node_affiliations: (node, callback) ->
if typeof node is 'string'
- channel = app.channels.get_or_create id:nodeid
- node = channel.nodes.get_or_create nodeid:nodeid
+ channel = app.channels.get_or_create id:node
+ node = channel.nodes.get_or_create nodeid:node
- rsm_after = node.subscribers_rsm_last
- @connector.get_node_subscriptions nodeid, rsm_after, callback
+ @get_affiliations_queue.add node, callback
+
+ get_all_node_affiliations: (nodeid, callback) =>
+ @get_node_affiliations nodeid, (err, results, done) =>
+ if err or done
+ callback?()
+ else
+ @get_all_node_affiliations nodeid, callback
+
+ set_channel_affiliation: (userid, affiliator, affiliation, callback) =>
+ forEachUserNode userid, (node, callback2) =>
+ @connector.set_node_affiliation node, affiliator, affiliation, callback
+ , callback
publish: (node, item, callback) ->
nodeid = node.get?('nodeid') or node
@@ -116,26 +141,55 @@ class exports.DataHandler extends Backbone.EventHandler
@connector.remove_from_roster user
callback? if oneSuccess then null else oneError
+ grant_subscription: (subscription, callback) ->
+ newSubscriptions = {}
+ newSubscriptions[subscription.get('id')] = 'subscribed'
+
+ async.forEach @get_pending_user_subscriptions(subscription)
+ , (subscription1, cb) =>
+ @connector.set_node_subscriptions subscription1.get('node')
+ , newSubscriptions, cb
+ , callback
+
+ deny_subscription: (subscription, callback) ->
+ newSubscriptions = {}
+ newSubscriptions[subscription.get('id')] = 'none'
+
+ async.forEach @get_pending_user_subscriptions(subscription)
+ , (subscription1, cb) =>
+ @connector.set_node_subscriptions subscription1.get('node')
+ , newSubscriptions, cb
+ , callback
+
+
+ # Looks if there are other nodes in the same channel that this
+ # user has pending subscription to.
+ get_pending_user_subscriptions: (subscription) ->
+ pending_subscriptions = []
+
+ channel = app.channels.get(subscription.get('node'))
+ channel.nodes.each (node) ->
+ subscription1 = node.subscribers.get subscription.get('id')
+ if subscription1?.get('subscription') is 'pending'
+ pending_subscriptions.push subscription1
+
+ pending_subscriptions
+
+ ##
+ # @param callback(error, done)
get_user_subscriptions: (jid, callback) =>
nodeid = "/user/#{jid}/subscriptions"
if jid isnt "anony@mous"
- rsmAfter = null
- step = =>
- @connector.get_node_posts nodeid, rsmAfter, (err, posts) =>
- # TODO: synced?
- if not posts?.rsm?.after or posts?.rsm?.after is rsmAfter
- # Final page
- app.users.get(jid).subscriptions_synced = app.users.current.channels.get(nodeid)?
- callback? err
- else
- # Next page
- rsmAfter = posts.rsm.after
- step()
- step()
+ @get_node_posts nodeid, callback
else
# anony@mous has no retrievable subscriptions
- callback?()
+ callback?(null, true)
+
+ get_all_user_subscriptions: (jid, callback) =>
+ @get_user_subscriptions jid, (err, results, done) =>
+ unless err or (not results?.length > 0) or done
+ @get_all_user_subscriptions jid, callback
# event callbacks
@@ -145,13 +199,6 @@ class exports.DataHandler extends Backbone.EventHandler
channel = app.channels.get_or_create id:nodeid
channel.push_post nodeid, post
- on_node_posts_rsm_last: (nodeid, rsmLast) =>
- channel = app.channels.get_or_create id:nodeid
- # FIXME: more indirection like above?
- node = channel.nodes.get_or_create id:nodeid
- # Push info to retrieve next page
- node.push_posts_rsm_last rsmLast
-
on_node_error: (nodeid, error) =>
channel = app.channels.get_or_create id:nodeid
channel.push_node_error nodeid, error
@@ -165,15 +212,29 @@ class exports.DataHandler extends Backbone.EventHandler
# Replay starting one day before last view
lastView = new Date(app.users.current.channels.get_last_timestamp())
mamStart = new Date(lastView - 23 * 60 * 60 * 1000).toISOString()
- pending = 2
- done = =>
- pending--
- if pending < 1
- @set_loading false
- @get_user_subscriptions app.users.current.get('id'), (error) =>
- done()
+
+ async.parallel [ (cb) =>
+ @get_all_user_subscriptions app.users.current.get('id'), cb
+ , (cb) =>
@scan_roster_for_channels()
- @connector.replayNotifications mamStart, done
+ # return immediately:
+ cb()
+ , (cb) =>
+ @connector.replayNotifications mamStart, cb
+ , (cb) =>
+ # Check what status nodes are left to load
+ # (we display them in the sidebar):
+ statusnodes = app.users.current.channels.map((channel) =>
+ channel.nodes.get_or_create(id: 'status')
+ ).filter((statusnode) =>
+ # Not loaded most recent post yet?
+ not (statusnode.posts.at(0)?)
+ )
+ async.forEach statusnodes, (statusnode, cb2) =>
+ @get_node_posts statusnode.get('nodeid'), cb2
+ , cb
+ ], =>
+ @set_loading false
on_connection_end: =>
app.channels.each (channel) ->
@@ -242,21 +303,18 @@ class exports.DataHandler extends Backbone.EventHandler
forEachUserNode userid, (nodeid, callback2) =>
node = channel.nodes.get_or_create nodeid:nodeid
- # 2: get_node_posts + get_node_metadata
- pending = 2
- done = ->
- pending--
- if pending < 1
- callback2()
-
- unless node.posts_synced
- @get_node_posts nodeid, done
- else
- done()
- unless node.metadata_synced
- @get_node_metadata nodeid, done
- else
- done()
+ async.parallel [ (callback3) =>
+ console.warn "refresh_channel", nodeid, node.posts_synced
+ unless node.posts_synced
+ @get_node_posts nodeid, callback3
+ else
+ callback3()
+ , (callback3) =>
+ unless node.metadata_synced
+ @get_node_metadata nodeid, callback3
+ else
+ callback3()
+ ], callback2
, =>
channel.set_loading false
callback?()
@@ -285,12 +343,12 @@ class exports.DataHandler extends Backbone.EventHandler
##
# @param iter {Function} callback(node, callback)
forEachUserNode = (user, iter, callback) ->
- pending = 0
- ["posts", "status", "subscriptions",
- "geo/previous", "geo/current", "geo/next"].forEach (type) ->
- nodeid = "/user/#{user}/#{type}"
- pending++
- iter nodeid, ->
- pending--
- if pending < 1
- callback?()
+ nodes = ("/user/#{user}/#{type}" for type in [
+ "posts",
+ "status",
+ "subscriptions",
+ "geo/previous",
+ "geo/current",
+ "geo/next",
+ ])
+ async.forEach nodes, iter, callback
View
38 src/handlers/rsm_queue.coffee
@@ -0,0 +1,38 @@
+class exports.RSMQueue
+ constructor: (@name, @req_cb) ->
+ @queued = {}
+
+ add: (node, cb) ->
+ id = node.get('nodeid') or node.get('id')
+ if @queued.hasOwnProperty(id)
+ @queued[id].push cb
+
+ else
+ rsm_info = node["#{@name}_rsm"]
+ unless rsm_info?
+ node["#{@name}_rsm"] = {}
+ node.bind 'unsync', =>
+ # Reset RSM:
+ delete node["#{@name}_rsm"]
+
+ if rsm_info?.end_reached
+ cb? null, [], yes
+
+ else
+ @queued[id] = [cb]
+ console.warn "rsm", @name, id, rsm_info?.last
+ @req_cb id, rsm_info?.last, (err, results) =>
+ console.warn "rsm", @name, id, results?.length
+
+ if not results?.rsm?.last? or rsm_info?.last is results?.rsm?.last
+ node["#{@name}_rsm"] = { end_reached: yes }
+ end_reached = yes
+ else
+ node["#{@name}_rsm"] = { last: results?.rsm?.last }
+ end_reached = no
+
+ queued = @queued[id]
+ delete @queued[id]
+
+ for cb1 in queued
+ cb1?(err, results, end_reached)
View
115 src/init.coffee
@@ -1,5 +1,5 @@
window.app =
- version: '0.0.0-47'
+ version: '0.0.0-57'
localStorageVersion:'9e5dcf0'
handler: {}
views: {}
@@ -17,9 +17,45 @@ require './vendor-bridge'
{ ChannelStore } = require './collections/channel'
{ UserStore } = require './collections/user'
formatdate = require 'formatdate'
+Notificon = require 'notificon'
+
+
+### could be used to switch console output ###
+app.debug_mode = config.debug ? on
+app.debug = ->
+ console.log "DEBUG:", arguments if app.debug_mode
+app.error = ->
+ console.error "DEBUG:", arguments if app.debug_mode
+Strophe.log = (level, msg) ->
+ console.warn "STROPHE:", level, msg if app.debug_mode and level > 0
+Strophe.fatal = (msg) ->
+ console.error "STROPHE:", msg if app.debug_mode
+
+
+
+# show a nice unread counter in the favicon
+total_number = 0
+app.favicon = (number) ->
+ total = total_number + number ? 0
+ return if total is total_number or isNaN(total)
+ console.warn "notificon", total
+ Notificon total or "",
+ font: "9px Helvetica"
+ stroke:"#F03D25"
+ color: "#ffffff"
+ total_number = total
+
+
# app bootstrapping on document ready
-$(document).ready ->
+app.initialize = ->
+
+ # when domain used an older webclient version before, we clear localStorage
+ version = localStorage.getItem('__version__')
+ unless app.localStorageVersion is version
+ localStorage.clear()
+ localStorage.setItem('__version__', app.localStorageVersion)
+
# show error message when config isnt loaded
if typeof config is 'undefined'
@@ -28,56 +64,39 @@ $(document).ready ->
.html(do require './templates/welcome/configerror.html')
return
- ### could be used to switch console output ###
- app.debug_mode = config.debug ? on
- app.debug = ->
- console.log "DEBUG:", arguments if app.debug_mode
- app.error = ->
- console.error "DEBUG:", arguments if app.debug_mode
- Strophe.log = (level, msg) ->
- console.warn "STROPHE:", level, msg if app.debug_mode and level > 0
- Strophe.fatal = (msg) ->
- console.error "STROPHE:", msg if app.debug_mode
+ # caches
+ app.channels = new ChannelStore
+ app.users = new UserStore # userstore depends on channelstore
- app.initialize = ->
+ # strophe handler
+ app.handler.connection = new ConnectionHandler
- # when domain used an older webclient version before, we clear localStorage
- version = localStorage.getItem('__version__')
- unless app.localStorageVersion is version
- localStorage.clear()
- localStorage.setItem('__version__', app.localStorageVersion)
-
- # caches
- app.channels = new ChannelStore
- app.users = new UserStore # userstore depends on channelstore
-
- # strophe handler
- app.handler.connection = new ConnectionHandler
+ ### the password hack ###
+ ### FIXME
+ Normally a webserver would return user information for a current session. But there is no such thing in buddycloud.
+ To achieve an auto-login we do a little trick here. Once a user has signed in, his browser asks him to store
+ the password for him. If the user accepts that, the login form will get filled automatically the next time he signs in.
+ So when something is typed into the form on document ready we know that it must be the stored password and can just submit the form.
+ ###
+ #el = $('#home_login_pwd')
+ #pw = el.val()
+ #unless pw.length > 0
+ # # the home view sould display some additional info in the future
+ # #app.views.home = new HomeView()
+ #else
+ # # prefilled password detected, sign in the user automatically
+ # $('#login_form').trigger "submit"
+ formatdate.options.max.unit = 9 # century
+ formatdate.options.max.amount = 20 # 2000 years
+ formatdate.hook '[data-date]'
+ $(document).ready ->
# page routing
app.router = new Router
- ### the password hack ###
- ### FIXME
- Normally a webserver would return user information for a current session. But there is no such thing in buddycloud.
- To achieve an auto-login we do a little trick here. Once a user has signed in, his browser asks him to store
- the password for him. If the user accepts that, the login form will get filled automatically the next time he signs in.
- So when something is typed into the form on document ready we know that it must be the stored password and can just submit the form.
- ###
- #el = $('#home_login_pwd')
- #pw = el.val()
- #unless pw.length > 0
- # # the home view sould display some additional info in the future
- # #app.views.home = new HomeView()
- #else
- # # prefilled password detected, sign in the user automatically
- # $('#login_form').trigger "submit"
- formatdate.options.max.unit = 9 # century
- formatdate.options.max.amount = 20 # 2000 years
- formatdate.hook '.time'
-
- Modernizr.load
- test:Modernizr.localStorage
- yep:'web/js/store.js'
- complete:app.initialize
+
+Modernizr.load
+ test:Modernizr.localStorage
+ yep:'web/js/store.js'
+ complete:app.initialize
View
23 src/models/channel.coffee
@@ -7,10 +7,11 @@
# Attribute jid: Jabber-Id
class exports.Channel extends Model
initialize: ->
+ @_unread_count = 0
@id = @get 'id'
@last_touched = new Date
@nodes = new NodeStore channel:this
- @avatar = gravatar @id, s:50, d:'retro'
+ @avatar = gravatar @id
@nodes.fetch()
# Auto-create the default set of nodes for that channel, so
@@ -28,7 +29,7 @@ class exports.Channel extends Model
# subscription.jid is already filtered for this channel id (user)
push_subscription: (subscription) ->
- # subscription.subscription is either subscribed, unsubscribed or pending
+ # subscription.subscription is either subscribed, none or pending
@trigger 'subscription', subscription
push_affiliation: (affiliation) ->
@@ -54,6 +55,8 @@ class exports.Channel extends Model
count++
else break
@trigger 'bubble'
+ app.favicon(count - @_unread_count) # only add new ones
+ @_unread_count = count
count
mark_read: ->
@@ -61,4 +64,20 @@ class exports.Channel extends Model
last_update = @nodes.get('posts').posts.at(0)?.get_last_update()
if last_update and last_update > last_view
last_view = last_update
+ app.favicon(@_unread_count * -1) # remove them
+ @_unread_count = 0
@save { last_view }
+
+ count_notifications: ->
+ if app.users.current.canModerate(this)
+ # Count users with pending subscription
+ postsnode = @nodes.get_or_create(id: 'posts')
+ postsnode.subscribers.reduce (count, subscription) ->
+ if subscription.get('subscription') is 'pending'
+ count + 1
+ else
+ count
+ , 0
+ else
+ # Isn't owner, no admin notifications
+ 0
View
4 src/models/metadata/node.coffee
@@ -2,7 +2,3 @@
class exports.NodeMetadata extends Metadata
type: 'node'
-
- query: ->
- app.handler.data.get_node_metadata @parent, (metadata) =>
- @save metadata
View
74 src/models/node/base.coffee
@@ -2,20 +2,25 @@
{ NodeMetadata } = require '../metadata/node'
{ Users } = require '../../collections/user'
{ Posts } = require '../../collections/post'
+{ Collection } = require '../../collections/base'
##
# Attributes:
# * id is only the tail for a channel (eg. posts)
# * nodeid is the full node name (eg. /user/astro@spaceboyz.net/posts)
class exports.Node extends Model
+ defaults:
+ nodeid:undefined
initialize: ->
nodeid = @get 'nodeid'
@metadata = new NodeMetadata parent:this, id:nodeid
- # Subscribers:
- @subscriptions = new Users parent:this
- @affiliations = new Users parent:this
- @posts ?= new Posts parent:this
+ @posts ?= new Posts parent:this
+ @posts.bind 'unsync', =>
+ @push_error null
+ # TODO: comparator by id
+ @subscribers = new Collection()
+ @affiliations = new Collection()
toJSON: (full) ->
result = super
@@ -29,20 +34,22 @@ class exports.Node extends Model
update: -> # api function - every node should be updateable
push_subscription: (subscription) ->
- switch subscription.subscription
- when 'subscribed'
- @subscriptions.get_or_create id: subscription.jid
- when 'unsubscribed', 'none'
- if (user = @subscriptions.get subscription.jid)
- @subscriptions.remove user
+ old_s = @subscribers.get(subscription.jid)?.get('subscription')
+
+ subscription.id ?= subscription.jid
+ subscription = @subscribers.get_or_create subscription
+ @trigger 'subscriber:update', subscription
+
+ s = subscription.get('subscription')
+ # Transition from/to subscribed
+ if s isnt old_s and (s is 'subscribed' or old_s is 'subscribed')
+ @trigger 'unsync'
push_affiliation: (affiliation) ->
- switch affiliation.affiliation
- when 'outcast', 'none'
- if (user = @affiliations.get subscription.jid)
- @affiliations.remove user
- else # owner, moderator, publisher, member
- @affiliations.get_or_create id: affiliation.jid
+ affiliation.id ?= affiliation.jid
+ affiliation = @affiliations.get_or_create affiliation
+
+ @trigger 'affiliation:update', affiliation
push_post: (post) ->
@trigger 'post', post
@@ -56,40 +63,7 @@ class exports.Node extends Model
@metadata_synced = no
push_error: (error) ->
- @error =
+ @error = error ?
condition: error.condition
text: error.text
@trigger 'error', error
-
- push_posts_rsm_last: (rsm_last) ->
- # No RSM support or
- # same <last/> as previous page
- @posts_end_reached = not rsm_last or
- rsm_last is @posts_rsm_last
- @posts_rsm_last = rsm_last
-
- # If we are subscribed, newer/updated posts will come in
- # through notifications. No need to poll again.
- # FIXME: clear on xmpp disconnect
- on_posts_synced: ->
- if app.users.current.channels.get(@get 'nodeid')?
- @posts_synced = yes
- else
- @posts_synced = no
-
- can_load_more_posts: ->
- not @posts_end_reached
-
- on_subscribers_synced: ->
- if app.users.current.channels.get(@get 'nodeid')?
- @subscribers_synced = yes
- else
- @subscribers_synced = no
-
- push_subscribers_rsm_last: (rsm_last) ->
- @subscribers_end_reached = not rsm_last or
- rsm_last is @subscribers_rsm_last
- @subscribers_rsm_last = rsm_last
-
- can_load_more_subscribers: ->
- not @subscribers_end_reached
View
71 src/models/user.coffee
@@ -9,7 +9,7 @@ class exports.User extends Model
# id and jid are the same
@id = @get('jid') or @get('id')
@save {jid: @id, @id}
- @avatar = gravatar @id, s:50, d:'retro'
+ @avatar = gravatar @id
# subscribed channels
@channels = new UserChannels parent:this
@metadata = new UserMetadata parent:this
@@ -24,3 +24,72 @@ class exports.User extends Model
isFollowing: (channel) ->
@channels.get(channel.get 'id')?
+
+ getSubscriptionFor: (channel) ->
+ if typeof channel is 'string'
+ channel = app.channels.get(channel)
+ node = channel.nodes.get_or_create(id: 'posts')
+ subscription = node?.subscribers.get(@get 'id')?.get('subscription')
+ subscription or 'none'
+
+ getAffiliationFor: (channel) ->
+ if typeof channel is 'string'
+ channel = app.channels.get(channel)
+ node = channel.nodes.get_or_create(id: 'posts')
+ affiliation = node?.affiliations.get(@get 'id')?.get('affiliation')
+ affiliation or 'none'
+
+ canPost: (channel) ->
+ return no if app.users.isAnonymous this
+
+ if typeof channel is 'string'
+ channel = app.channels.get(channel)
+
+ affiliation = @getAffiliationFor(channel)
+ subscription = @getSubscriptionFor(channel)
+ metadata = channel.nodes.get('posts')?.metadata
+ publish_model = metadata?.get('publish_model')?.value
+
+ switch publish_model
+ when 'open'
+ return yes
+ when 'subscribers'
+ return subscription == 'subscribed'
+ when 'publishers'
+ return isAffiliationAtLeast affiliation, 'publisher'
+ else
+ return no
+
+ canEdit: (channel) ->
+ return no if app.users.isAnonymous this
+
+ @getAffiliationFor(channel) == 'owner'
+
+ canModerate: (channel) ->
+ return no if app.users.isAnonymous this
+
+ if typeof channel is 'string'
+ channel = app.channels.get(channel)
+
+ postsnode = channel.nodes.get_or_create(id: 'posts')
+ if @getAffiliationFor(channel) == 'moderator' and
+ postsnode.metadata.get('channel_type')?.value is 'topic'
+ yes
+ else if @getAffiliationFor(channel) == 'owner'
+ yes
+ else
+ no
+
+
+# Copied from server operations
+AFFILIATIONS = [
+ 'outcast', 'none', 'member',
+ 'publisher', 'moderator', 'owner'
+]
+isAffiliationAtLeast = (affiliation1, affiliation2) ->
+ i1 = AFFILIATIONS.indexOf(affiliation1)
+ i2 = AFFILIATIONS.indexOf(affiliation2)
+ if i2 < 0
+ false
+ else
+ i1 >= i2
View
15 src/styles/channels.styl
@@ -188,6 +188,21 @@
.channel
transition 500ms height
+#create_topic_channel
+ width 232px
+ bottom 0
+ margin-left 1px
+ color white
+ line-height 27px
+ text-align center
+ text-shadow 0 1px black
+ border-top 1px solid black
+ background rgba(0,0,0,.89)
+ box-shadow 0 1px 0 1px black, inset 0 1px rgba(255,255,255,.21)
+ cursor pointer
+ position fixed
+ z-index 9
+
#more_channels, .channelOverview #more_channels
width 231px
bottom 0
View
1  src/styles/edit.styl
@@ -61,6 +61,7 @@
[contenteditable="true"],
.contenteditable
+ text-overflow clip !important
color black !important
background #fe0 !important
border-radius 2px
View
2  src/styles/forms.styl
@@ -45,7 +45,7 @@ form
.spinner
padding-left 23px
color rgba(0,0,0,.55)
- background url(../../public/images/spinner_white.gif) no-repeat 0 5px
+ background url(../../public/spinner.gif) no-repeat 0 5px
display none
&.working
.resetPassword, input
View
11 src/styles/main.styl
@@ -110,4 +110,13 @@ table
font-size 27px
color white
text-align center
- text-shadow 0 0 1px rgba(0, 0, 0, 0.89), 0 1px 5px rgba(0, 0, 0, 0.55)
+ text-shadow 0 0 1px rgba(0, 0, 0, 0.89), 0 1px 5px rgba(0, 0, 0, 0.55)
+
+.spinner
+ width 16px
+ height 16px
+ margin 17px 8px 0 0
+ vertical-align top
+ background url(../../public/spinner.gif) no-repeat
+ display inline-block
+
View
27 src/styles/stream.styl
@@ -24,7 +24,12 @@
width 76px
height 26px
text-indent -9999px
- background url(../../public/images/poweredby.png)
+ background url(../../public/images/poweredby.png) no-repeat
+ color black
+ text-decoration none
+ &:hover
+ padding-top 20px
+ text-indent 0
position absolute
header.small #poweredby
top 0
@@ -159,7 +164,7 @@
width 500px
border-radius 5px 5px
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.08), 0 0 13px rgba(0, 0, 0, 0.05)
- float left
+ //float left
position relative
z-index 2
@@ -193,7 +198,7 @@
box-shadow none
.notification
height 49px
- margin-top -76px
+ margin-top -49px
color white
text-shadow 0 1px 1px rgba(0,0,0,.34)
border-top 1px solid rgba(0, 0, 0, 0.13)
@@ -721,9 +726,12 @@
overflow hidden
.channelInfo
padding 5px 8px
- margin-bottom 1px
+ // margin-bottom 1px FIXME really necessary?
background linear-gradient(top, #fafafa, #f7f7f7)
box-shadow 0 1px 1px rgba(0, 0, 0, 0.34)
+ h4
+ // JID link
+ cursor pointer
.action
padding 5px 8px
margin-bottom 1px
@@ -748,6 +756,7 @@
&.negative
background url(../../public/icons/minus.png) no-repeat 2px center
.actionRow
+ display none
width 100%
box horizontal justify center
&.confirm
@@ -772,18 +781,22 @@
padding 6px 5px 4px
background linear-gradient(top, #f7f7f7, #ccc)
box-shadow 0 2px white inset, 0 1px rgba(0, 0, 0, 0.05)
+ &.moderator
+ .actionRow
+ display block
&.choosen
.actionRow
display none
- &.confirm
- box horizontal justify center
+ &.choosen.moderator
+ .actionRow.confirm
+ box horizontal justify center
&.role
.changeRole
display block
&.ban
.banUser
display block
-
+
.count
font-size 14px
line-height 27px
View
9 src/templates/channel/comments.coffee
@@ -15,7 +15,14 @@ module.exports = design (view) ->
@$section class:'comments', ->
# <% if @user?.hasRightToPost: %> FIXME
@$section class:'answer', ->
- return @remove() if app.users.isAnonymous(app.users.current)
+ update_answer = =>
+ if app.users.current.canPost(view.parent.parent.parent.model)
+ @show()
+ else
+ @hide()
+ view.parent.parent.parent.bind 'update:permissions', update_answer
+ update_answer()
+
@$img class:'avatar', ->
@attr src:"#{app.users.current.avatar}"
# textarea
View
45 src/templates/channel/details/index.coffee
@@ -9,11 +9,15 @@ unless process.title is 'browser'
{ Template } = require 'dynamictemplate'
jqueryify = require 'dt-jquery'
+{ throttle_callback } = require '../../../util'
design = require '../../../_design/channel/details/index'
module.exports = design (view) ->
return jqueryify new Template schema:5, ->
+ postsnode = view.model.nodes.get_or_create(id: 'posts')
+ metadata = postsnode.metadata
+
@$div class: 'channelDetails', ->
@$div class: 'holder', ->
@$section class: 'meta', ->
@@ -29,17 +33,38 @@ module.exports = design (view) ->
creationDate = make_field 'broadcast'
update_metadata = =>
- console.warn "update_metadata", view.metadata
- owner.text view.model.get('id')
- metadata = view.metadata.toJSON()
- description.text metadata.description?.value
- accessModel.text metadata.access_model?.value
- date = metadata.creation_date?.value
+ owners = postsnode.affiliations.filter (affiliation) ->
+ affiliation.get('affiliation') is 'owner'
+ owner.text owners.map((owner) -> owner.get 'id').
+ join(" ")
+ description.text metadata.get('description')?.value
+ if metadata.get('access_model')?.value is 'open'
+ accessModel.text "open"
+ else
+ accessModel.text "private"
+ date = metadata.get('creation_date')?.value
if date?
creationDate.attr "data-date":date
- creationDate._jquery.formatdate(update:off)
- view.metadata.bind 'change', update_metadata
+ creationDate._jquery?.formatdate(update:off)
+ # Filtering for owners takes potentially long, and
+ # we bind to every affiliation update.
+ update_metadata_callback = throttle_callback 400, update_metadata
+ view.metadata.bind 'change', update_metadata_callback
+ postsnode.bind 'affiliation:update', update_metadata_callback
update_metadata()
- view.bind('subview:followers', @add)
- view.bind('subview:following', @add)
+ view.bind 'subview:moderators', @add
+ view.bind 'subview:followers', @add
+ view.bind 'subview:following', (el) =>
+ @add el
+
+ update_visibility = ->
+ # FIXME: uses the jQuery `el'. the `@add()'
+ # result didn't work.
+ if metadata.get('channel_type')?.value is 'topic'
+ # Topic channels don't follow anyone
+ el.hide()
+ else
+ el.show()
+ metadata.bind 'change', update_visibility
+ update_visibility()
View
65 src/templates/channel/details/list.coffee
@@ -10,7 +10,7 @@ unless process.title is 'browser'
{ Template } = require 'dynamictemplate'
jqueryify = require 'dt-jquery'
design = require '../../../_design/channel/details/list'
-{ EventHandler } = require '../../../util'
+{ EventHandler, gravatar } = require '../../../util'
module.exports = design (view) ->
@@ -20,47 +20,36 @@ module.exports = design (view) ->
@$h3 ->
@text "#{view.title} "
@$span class: 'count', ->
+ @hide()
+
update_count = =>
- @text view.model.length
+ @text "#{view.showing_count}"
+ view.bind 'show:all', =>
+ @show()
+ update_count()
update_count()
- list = @$div class: 'list'
- add_follower = (user) ->
- userid = user.get 'id'
- imgBefore = null
- list._jquery.find(".avatar").each ->
- existingImg = $(this)
- existingId = existingImg.data('userid')
- if existingId < userid and
- (not imgBefore or existingId > imgBefore.data('userid'))
- imgBefore = existingImg
- img = $('<img>')
- img.attr
- class:'avatar'
- src: "#{user?.avatar}",
- title: user.get('id')
- 'data-userid': user.get('id')
- if imgBefore
- img.insertAfter imgBefore
- else
- list._jquery.prepend img
- img.click EventHandler ->
- app.router.navigate user.get('id'), true
- rm_follower = (userid) ->
- list._jquery.find('img').each ->
- img = $(this)
- if img.data('userid') is userid
+ list = @$div class: 'list', ->
+
+ new_user = (user) =>
+ uid = user.get('id')
+ img = @$img
+ class:'avatar'
+ src:"#{gravatar uid}"
+ title:uid
+ 'data-userid': uid # FIXME UGLY
+ return remove:->
img.remove()
- # Iterates through node.subscriptions in "followers" case,
- # and over user.channels in "following" case:
- view.model.forEach (user) ->
- add_follower user
- view.model.bind 'add', (user) ->
- add_follower user
- update_count()
- view.model.bind 'remove', (user) ->
- rm_follower user.get('id')
- update_count()
+
+ users = {}
+ view.bind 'add', (user) ->
+ users[user.get('id')] ?= new_user(user)
+ update_count?()
+ view.bind 'remove', (user) ->
+ users[user.get('id')]?.remove()
+ delete users[user.get('id')]
+ update_count?()
@$div class: 'showAll', ->
view.bind 'show:all', =>
@remove()
+
View
157 src/templates/channel/details/user.coffee
@@ -0,0 +1,157 @@
+unless process.title is 'browser'
+ return module.exports =
+ src: "streams.html"
+ select: () ->
+ el = @select "section.channelList .adminAction"
+ el.find('.channelInfo, .currentRole').text ""
+ el.removeClass('moderator')
+ # FIXME everywhere: "chosen"?
+ el.removeClass('choosen')
+ el.removeClass('role')
+ el
+
+
+{ Template } = require 'dynamictemplate'
+jqueryify = require 'dt-jquery'
+design = require '../../../_design/channel/details/user'
+{ EventHandler, throttle_callback } = require '../../../util'
+
+userspeak =
+ 'owner': "Producer"
+ 'moderator':"Moderator"
+ 'publisher':"Follower+Post"
+ 'member': "Follower"
+ 'outcast': "Banned"
+ 'none': "Does not follow back"
+
+affiliations_infos =
+ owner: [
+ "read your channel"
+ "write comments & messages"
+ "post new topics"
+ "add & ban users"
+ "set user's roles"
+ ]
+ moderator: [
+ "approve new followers"
+ "delete posts"
+ "read your channel"
+ "write comments & messages"
+ "post new topics"
+ ]
+ publisher: [
+ "read your channel"
+ "write comments & messages"
+ "post new topics"
+ ]
+ member: [
+ "read your channel"
+ ]
+ outcast: [
+ "forbidden to read your channel"
+ ]
+ none: [
+ "read your open channel"
+ ]
+
+module.exports = design (view) ->
+ return jqueryify new Template schema:5, ->
+ channel = view.parent.parent.model
+ set_info_lines = ->
+ @$div class:'adminAction', ->
+ # .arrow
+ @$div class:'holder', ->
+ @$div class:'box', ->
+ @$section class:'channelInfo', ->
+ name = @$h4()
+ role = @$div class:'currentRole'
+ view.bind 'user:update', (user) ->
+ name.text "#{user.get 'id'}"
+ affiliation = user.getAffiliationFor channel.get 'id'
+ role?.text "#{userspeak[affiliation] or affiliation}"
+ update_role = =>
+ if app.users.current.canEdit channel
+ role.show()
+ else
+ role.hide()
+ view.parent.parent.parent.bind 'update:permissions', update_role
+ update_role()
+
+ @$section class:'action changeRole', ->
+ @$select ->
+ @$option value: 'moderator', ->
+ @remove()
+ @$option value: 'followerPlus', ->
+ @remove()
+ @$option value: 'follower', ->
+ @remove()
+
+ options = {}
+ for own value, text of userspeak
+ postsnode = view.parent.parent.model.nodes.get_or_create(id: 'posts')
+ unless value is 'none'
+ @$option {value}, ->
+ @text text
+ options[value] = this
+ current_user = null
+ set_current_option = =>
+ affiliation = postsnode.affiliations.get(current_user)?.get('affiliation')
+ console.warn "set_current_option", current_user, affiliation
+ # FIXME: Preselecting the current <option/> doesn't work like this :-(
+ @attr 'value', affiliation
+ set_info_lines(affiliations_infos[affiliation] or [])
+ set_current_option_callback = throttle_callback 100, set_current_option
+ view.bind 'user:update', (user) ->
+ current_user = user
+ set_current_option_callback()
+ view.parent.parent.parent.bind 'update:affiliations', set_current_option_callback
+ set_current_option()
+
+ view.bind 'update:select:affiliation', =>
+ # FIXME: couldn't we use dt like
+ # above, just in a way that actually
+ # works?
+ set_info_lines(affiliations_infos[@attr('value')] or [])
+
+ @$section class: 'info', ->
+ @$div class: 'moderator', ->
+ @remove()
+
+ @$div class: 'moderator', ->
+ @$ul ->
+ old_lines = []
+ set_info_lines = (lines) =>
+ for old_line in old_lines
+ old_line.remove()
+ old_lines = []
+ for line in lines
+ old_lines.push @$li ->
+ @text line
+
+ @$section class: 'action banUser', ->
+ @remove()
+ @$section class: 'actionRow choose', ->
+ @remove()
+
+ @$section class: 'actionRow confirm', ->
+ @$div ->
+ @attr class: 'cancelButton'
+ @$div ->
+ @attr class: 'okButton'
+
+ update_role = =>
+ classes = @attr('class').split(/\s+/)