git.fiddlerwoaroof.com
Browse code

Merge branch 'production'

fiddlerwoaroof authored on 02/02/2016 05:16:05
Showing 57 changed files
... ...
@@ -14,6 +14,24 @@ END
14 14
 $$ LANGUAGE plpgsql;
15 15
 
16 16
 
17
+DROP FUNCTION IF EXISTS subscribe_link(text,int);
18
+CREATE OR REPLACE FUNCTION subscribe_link(username text, linkid int)
19
+  RETURNS bool AS $$
20
+DECLARE
21
+  uid int;
22
+  result bool;
23
+  n_title text;
24
+  n_url text;
25
+BEGIN
26
+  SELECT INTO result NOT exists(SELECT * FROM user_links WHERE user_id=uid AND link_id=linkid);
27
+  IF result THEN
28
+    SELECT title,url INTO n_title,n_url FROM links WHERE links.id=linkid;
29
+    PERFORM put_link(username, n_url, n_title);
30
+  END IF;
31
+  RETURN result;
32
+END
33
+$$ LANGUAGE plpgsql;
34
+
17 35
 DROP FUNCTION IF EXISTS delete_link(text,int);
18 36
 CREATE OR REPLACE FUNCTION delete_link(username text, linkid int)
19 37
   RETURNS bool AS $$
... ...
@@ -30,10 +48,25 @@ BEGIN
30 48
 END
31 49
 $$ LANGUAGE plpgsql;
32 50
 
51
+DROP FUNCTION IF EXIST has_user_shared(text, text);
52
+CREATE OR REPLACE FUNCTION has_user_shared(username text, linkurl text) RETURNS bool AS $$
53
+DECLARE
54
+  result bool;
55
+BEGIN
56
+  SELECT INTO result EXISTS(
57
+    SELECT 1 FROM user_links ul
58
+    LEFT JOIN users u ON user_id=u.id
59
+    LEFT JOIN links l ON link_id=l.id
60
+    WHERE linkurl=l.url AND username=u.name
61
+  );
62
+  RETURN result;
63
+END
64
+$$ LANGUAGE plpgsql;
65
+
33 66
 DROP FUNCTION IF EXISTS get_bones(text, timestamp, int);
34 67
 DROP FUNCTION IF EXISTS get_bones(text, int, timestamp);
35 68
 CREATE OR REPLACE FUNCTION get_bones(username text, lim int, before timestamp)
36
-  RETURNS TABLE(url text, title text, posted timestamp, poster text, total_votes bigint, subscriber_vote int) AS $$
69
+  RETURNS TABLE(linkid int, url text, title text, posted timestamp, poster text, total_votes bigint, subscriber_vote int, shared bool) AS $$
37 70
 DECLARE
38 71
   subscriber_id int;
39 72
 BEGIN
... ...
@@ -43,9 +76,12 @@ BEGIN
43 76
     AS
44 77
       SELECT
45 78
         DISTINCT ON (links.url)
46
-        links.url,links.title,links.posted,users1.name,total_votes(links.id),user_vote(subscriber_id,links.id)
79
+        links.id,links.url,links.title,links.posted,users1.name,
80
+        total_votes(links.id),user_vote(subscriber_id,links.id),
81
+        has_user_shared(username, links.url)
47 82
         FROM user_subscriptions
48 83
         RIGHT JOIN user_links ON user_subscriptions.to_id=user_links.user_id
84
+        INNER JOIN links ON link_id=links.id
49 85
         INNER JOIN users ON users.id=fro_id
50 86
         LEFT JOIN users as users1 ON users1.id=to_id
51 87
         WHERE fro_id = subscriber_id
... ...
@@ -57,7 +93,8 @@ $$ LANGUAGE plpgsql;
57 93
 
58 94
 DROP FUNCTION IF EXISTS get_bones(text, int);
59 95
 CREATE OR REPLACE FUNCTION get_bones(username text, lim int)
60
-  RETURNS TABLE(url text, title text, posted timestamp, poster text, total_votes bigint, subscriber_vote int) AS $$
96
+  RETURNS TABLE(linkid int, url text, title text, posted timestamp, poster text, total_votes bigint,
97
+                subscriber_vote int, shared bool) AS $$
61 98
 DECLARE
62 99
   subscriber_id int;
63 100
 BEGIN
... ...
@@ -67,7 +104,9 @@ BEGIN
67 104
     AS
68 105
       SELECT
69 106
         DISTINCT ON (links.url)
70
-        links.url,links.title,links.posted,users1.name,total_votes(links.id),user_vote(subscriber_id,links.id)
107
+        links.id,links.url,links.title,links.posted,users1.name,
108
+        total_votes(links.id),user_vote(subscriber_id,links.id),
109
+        has_user_shared(username, links.url)
71 110
         FROM user_subscriptions
72 111
         RIGHT JOIN user_links ON user_subscriptions.to_id=user_links.user_id
73 112
         INNER JOIN links ON link_id=links.id
... ...
@@ -80,7 +119,8 @@ $$ LANGUAGE plpgsql;
80 119
 
81 120
 DROP FUNCTION IF EXISTS get_bones(text);
82 121
 CREATE OR REPLACE FUNCTION get_bones(username text)
83
-  RETURNS TABLE(url text, title text, posted timestamp, poster text, total_votes bigint, subscriber_vote int) AS $$
122
+  RETURNS TABLE(linkid int, url text, title text, posted timestamp, poster text, total_votes bigint,
123
+                subscriber_vote int, shared bool) AS $$
84 124
 DECLARE
85 125
   subscriber_id int;
86 126
 BEGIN
... ...
@@ -90,7 +130,9 @@ BEGIN
90 130
     AS
91 131
       SELECT
92 132
         DISTINCT ON (links.url)
93
-        links.url,links.title,links.posted,users1.name,total_votes(links.id),user_vote(subscriber_id,links.id)
133
+        links.id,links.url,links.title,links.posted,users1.name,
134
+        total_votes(links.id),user_vote(subscriber_id,links.id),
135
+        has_user_shared(username, links.url)
94 136
         FROM user_subscriptions
95 137
         RIGHT JOIN user_links ON user_subscriptions.to_id=user_links.user_id
96 138
         INNER JOIN links ON link_id=links.id
... ...
@@ -103,7 +145,7 @@ $$ LANGUAGE plpgsql;
103 145
 
104 146
 DROP FUNCTION IF EXISTS get_bone(text);
105 147
 CREATE OR REPLACE FUNCTION get_bone(username text)
106
-  RETURNS TABLE(name text, url text, title text, posted timestamp, linkid int, votes bigint) AS $$
148
+  RETURNS TABLE(name text, url text, title text, posted timestamp, linkid int, votes bigint, shared bool) AS $$
107 149
 BEGIN
108 150
   RETURN QUERY SELECT users.name, links.url, links.title, links.posted, links.id, total_votes(links.id)
109 151
       FROM users
... ...
@@ -380,3 +422,4 @@ BEGIN
380 422
   RETURN result;
381 423
 END
382 424
 $$ LANGUAGE plpgsql;
425
+
... ...
@@ -56,3 +56,14 @@ CREATE TABLE user_ak (
56 56
   UNIQUE (user_id, ak),
57 57
   PRIMARY KEY (id)
58 58
 );
59
+
60
+DROP VIEW IF EXISTS recently_active_users;
61
+CREATE VIEW recently_active_users AS
62
+WITH recent_users AS (
63
+  SELECT user_id,name,posted
64
+  FROM user_links
65
+  LEFT JOIN links ON link_id = links.id
66
+  RIGHT JOIN users ON user_id=users.id
67
+  WHERE posted > now() - interval '1 week'
68
+  ORDER BY posted desc, user_id)
69
+SELECT DISTINCT ON (user_id) user_id,name,posted FROM recent_users;
... ...
@@ -1,12 +1,13 @@
1 1
 Flask==0.10.1
2
-Flask-Cors==2.1.0
3
-Flask-Limiter==0.8.1
4
-Flask-Login==0.3.0
2
+Flask-Cors==2.1.2
3
+Flask-Limiter==0.9.1
4
+Flask-Login==0.3.2
5 5
 Flask-OAuth==0.12
6
-Flask-Security==1.7.4
7
-Flask-WTF==0.11
8
-lxml==3.4.4
6
+Flask-Security==1.7.5
7
+Flask-WTF==0.12
8
+lxml==3.5.0
9 9
 psycopg2==2.6.1
10 10
 python-dateutil==2.4.2
11
-textblob==0.9.1
12
-uWSGI==2.0.11
11
+requests==2.9.1
12
+textblob==0.11.0
13
+uWSGI==2.0.12
13 14
new file mode 100644
... ...
@@ -0,0 +1,2 @@
1
+marrow
2
+marrowpass
... ...
@@ -19,9 +19,11 @@ except ImportError:
19 19
         secret_key = base64.b64encode(os.urandom(24))
20 20
         debug = False
21 21
         static_root =  os.path.join(os.path.dirname(__file__), os.path.pardir, 'static')
22
+        server_name = "localhost"
22 23
 
23 24
 app.secret_key = config.secret_key
24 25
 app.debug = config.debug
26
+app.config["SERVER_NAME"] = config.server_name
25 27
 
26 28
 limiter = Limiter(app)
27 29
 limiter.limit("60/hour 3/second", key_func=lambda: request.host)(user.user_blueprint)
... ...
@@ -1,5 +1,6 @@
1 1
 import flask
2 2
 from flask import Blueprint, session, redirect, url_for, escape, request, abort, g
3
+import flask_login;
3 4
 from flask.ext.cors import cross_origin
4 5
 from flask.ext.login import login_required, current_user
5 6
 import urllib2
... ...
@@ -31,7 +32,7 @@ def as_json(f):
31 32
         return res
32 33
     return _inner
33 34
         
34
-@bone_blueprint.route('/link/<linkid>', methods=['GET','DELETE'])
35
+@bone_blueprint.route('/link/<linkid>', methods=['GET','POST','DELETE'])
35 36
 @login_required
36 37
 def delete_link(linkid):
37 38
     db = database.get_db()
... ...
@@ -42,6 +43,11 @@ def delete_link(linkid):
42 43
             cur.execute('SELECT id,url,title,posted FROM links WHERE id=%s', (linkid,))
43 44
             nid,url,title,posted = cur.fetchone()
44 45
             result = dict(id=nid,url=url,title=title,posted=posted.isoformat())
46
+        elif request.method == 'POST':
47
+            result = False
48
+            if 'username' in session:
49
+                cur.execute('SELECT subscribe_link(%s,%s)', (current_user.id,linkid))
50
+                result = cur.fetchone()[0]
45 51
         elif request.method == 'DELETE':
46 52
             result = False
47 53
             if 'username' in session:
... ...
@@ -56,7 +62,7 @@ def clean_url(url):
56 62
         netloc, path = path, netloc
57 63
     return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
58 64
 
59
-def get_title(url):
65
+def get_siteinfo(url):
60 66
     return config.titlegetter.get_title(url)
61 67
 
62 68
 @bone_blueprint.route('/vote/total')
... ...
@@ -135,7 +141,8 @@ def submit_link():
135 141
     if username is not None:
136 142
         url, title = obj['url'],obj['title']
137 143
         url = clean_url(url)
138
-        title = get_title(url)
144
+        title, url = get_siteinfo(url) # this makes sure that the url is the site's preferred URL
145
+                                       #  TODO: this might need sanity checks . . . like make sure same site?
139 146
         with db.cursor() as cur:
140 147
             cur.callproc('put_link', (username, url, title))
141 148
             ## This returns (link_id, user_id)
... ...
@@ -151,9 +158,9 @@ def submit_link():
151 158
 @bone_blueprint.route('', methods=['GET'])
152 159
 @login_required
153 160
 def default_data():
154
-    result = '', 401, {}
155
-    if 'username' in session:
156
-        result = data(current_user.id)
161
+    print current_user.id
162
+    print 'username' in session
163
+    result = data(current_user.id)
157 164
     return result
158 165
 
159 166
 @bone_blueprint.route('/u/<username>', methods=['GET'])
... ...
@@ -162,10 +169,14 @@ def data(username):
162 169
 
163 170
     result = {'marrow':[], 'sectionTitle': sectionTitle}
164 171
     with database.get_db().cursor() as cur:
165
-        cur.execute("SELECT url, title, posted, linkid, votes from get_bone(%s);", (username,))
172
+        cur_username = 'anonymous'
173
+        if current_user.is_authenticated:
174
+            cur_username = current_user.id
175
+        cur.execute("SELECT url, title, posted, linkid, votes, has_user_shared(%s, url) from get_bone(%s);",
176
+                    (cur_username, username,))
166 177
         result['marrow'] = [
167
-                dict(id=linkid, url=url,title=title,posted=posted.isoformat(),votes=votes)
168
-                     for url,title,posted,linkid,votes
178
+                dict(id=linkid, url=url,title=title,posted=posted.isoformat(),votes=votes,shared=shared)
179
+                     for url,title,posted,linkid,votes,shared
169 180
                      in cur.fetchall()
170 181
         ]
171 182
     return json.dumps(result)
... ...
@@ -229,8 +240,9 @@ def subscriptions(before, count):
229 240
                 args = args + (before,)
230 241
             cur.callproc("get_bones", args)
231 242
             result['marrow'] = [
232
-                dict(poster=poster, url=url,title=title,posted=posted.isoformat(), votes=votes, myVote=myvote)
233
-                    for url,title,posted,poster,votes,myvote
243
+                dict(id=id,poster=poster, url=url,title=title,posted=posted.isoformat(), votes=votes,
244
+                     myVote=myvote, shared=shared)
245
+                    for id,url,title,posted,poster,votes,myvote,shared
234 246
                     in cur.fetchall()
235 247
             ]
236 248
     return (json.dumps(result), 200, {'Content-Type': 'application/json'})
... ...
@@ -1,28 +1,31 @@
1 1
 from flask import g
2 2
 import psycopg2
3
+
3 4
 try: from marrow_config import config
4 5
 except ImportError:
5 6
     class config:
6
-        db = "marrow"
7
-        user = "marrow"
8
-        password = "marrowpass"
9
-        host = "pgsqlserver.elangley.org"
7
+        class db:
8
+            db = "marrow"
9
+            user = "marrow"
10
+            password = "marrowpass"
11
+            host = "pgsqlserver.elangley.org"
10 12
 
11
-def get_db():
13
+def get_db(close=True):
12 14
     db = getattr(g, '_database', None)
15
+    _config = config.db
13 16
     if db is None:
14
-        db = g._database = psycopg2.connect(
15
-          database=config.db,
16
-          user=config.user,
17
-          password=config.password,
18
-          host=config.host
19
-        );
20
-    return db
17
+        db = g._database = [psycopg2.connect(
18
+          database=_config.db,
19
+          user=_config.user,
20
+          password=_config.password,
21
+          host=_config.host
22
+        ),close];
23
+    return db[0]
21 24
 
22 25
 def close_connection(exception):
23 26
     db = getattr(g, '_database', None)
24
-    if db is not None:
25
-        db.close()
27
+    if db is not None and db[1]:
28
+        db[0].close()
26 29
 
27 30
 def check_ak(db, username, ak):
28 31
     with db.cursor() as cur:
... ...
@@ -1,7 +1,8 @@
1
-import lxml.html
2
-import urllib2
3
-import urlparse
4 1
 import re
2
+import urlparse
3
+
4
+import lxml.html
5
+import requests
5 6
 
6 7
 from textblob import TextBlob
7 8
 from textblob_aptagger import PerceptronTagger
... ...
@@ -12,21 +13,41 @@ def titlecase(line):
12 13
 
13 14
 class DefaultTitleGetter(object):
14 15
     url_cleaner = re.compile('[+\-_]')
16
+    user_agent = {'User-Agent': 'Marrow Title Getter: https://joinmarrow.com'}
15 17
 
16 18
     def get_title(self, url):
19
+        s = requests.session()
17 20
         scheme, netloc, path, params, query, fragment = urlparse.urlparse(url, 'http')
18
-        data = urllib2.urlopen(url)
19
-        content_type = data.headers['content-type'].lower()
20
-        charset = 'utf-8'
21
-        if 'charset' in content_type:
22
-            charset = content_type.partition('charset=')[-1]
23
-        data = data.read()
24
-        data = data.decode(charset)
25
-        etree = lxml.html.fromstring(data)
26
-        titleElems = etree.xpath('//title')
27
-        title = url
28
-        if titleElems != []:
29
-            title = titleElems[0].text
21
+        data = s.get(url, headers=self.user_agent)
22
+        etree = lxml.html.fromstring(data.content.decode(data.encoding))
23
+
24
+        canonicalLink = etree.xpath('//link[@rel="canonical"]/@href')
25
+        oetree = etree
26
+        if canonicalLink != []:
27
+            canonicalLink = canonicalLink[0]
28
+            try:
29
+                data = s.get(canonicalLink, headers=self.user_agent)
30
+                etree = lxml.html.fromstring(data.content.decode(data.encoding))
31
+            except requests.exceptions.MissingSchema:
32
+                nscheme, nnetloc, npath, nparams, nquery, nfragment = urlparse.urlparse(canonicalLink)
33
+                if nscheme == '':
34
+                    nscheme = scheme
35
+                if nnetloc == '':
36
+                    nnetloc = netloc
37
+                canonicalLink = urlparse.urlunparse((nscheme, nnetloc, npath, nparams, nquery, nfragment))
38
+                try:
39
+                    data = s.get(canonicalLink, headers=self.user_agent)
40
+                    etree = lxml.html.fromstring(data.content.decode(data.encoding))
41
+                except IOError:
42
+                    etree = oetree
43
+            except IOError:
44
+                etree = oetree
45
+        else:
46
+            canonicalLink = url
47
+
48
+        title = etree.xpath('//title/text()')
49
+        if title != []:
50
+            title = title[0]
30 51
         elif path:
31 52
             # hacky way to make a title
32 53
             path = urlparse.unquote(path)
... ...
@@ -37,4 +58,4 @@ class DefaultTitleGetter(object):
37 58
             title = map(titlecase, path)
38 59
             title = u' \u2014 '.join(title)
39 60
             title = u' \u2014 '.join([title, netloc])
40
-        return title
61
+        return title, canonicalLink
... ...
@@ -2,6 +2,7 @@ import urlparse
2 2
 import urllib2
3 3
 import json
4 4
 
5
+# TODO: this should use the articlesearch API, if this is actually necessary
5 6
 class TimesTitleGetter(object):
6 7
     api_url='http://api.nytimes.com/svc/news/v3/content.json?url=%(url)s&api-key=%(api_key)s'
7 8
     site='nytimes.com'
... ...
@@ -13,4 +14,4 @@ class TimesTitleGetter(object):
13 14
         info = json.load(urllib2.urlopen(api_url))
14 15
         title = info['results'][0]['title']
15 16
         source = info['results'][0]['source']
16
-        return u'%s \u2014 %s' % (title, source)
17
+        return u'%s \u2014 %s' % (title, source), url
... ...
@@ -1,4 +1,5 @@
1 1
 import urllib2
2
+import requests
2 3
 import urlparse
3 4
 
4 5
 from .utils import memoize
... ...
@@ -34,11 +35,10 @@ class TitleGetter(object):
34 35
                 handler = self.getters[site]
35 36
                 break
36 37
 
37
-        title = None
38 38
         try:
39
-            title = handler.get_title(url)
40
-        except urllib2.HTTPError:
41
-            title = self.default_handler.get_title(url)
39
+            title, canonicalUrl = handler.get_title(url)
40
+        except requests.exceptions.RequestException:
41
+            title, canonicalUrl = self.default_handler.get_title(url)
42 42
 
43
-        return title.encode('utf-8')
43
+        return title.encode('utf-8'), canonicalUrl
44 44
 
... ...
@@ -111,6 +111,7 @@ def adduser():
111 111
                 session['username'] = username
112 112
                 result['status'] = True
113 113
                 _get_users()
114
+                login_user(User.get_user(username))
114 115
             except psycopg2.IntegrityError as e:
115 116
                 db.rollback()
116 117
                 if e.pgcode == '23505': #username not unique
... ...
@@ -121,6 +122,23 @@ def adduser():
121 122
             else: db.commit()
122 123
     return json.dumps(result)
123 124
 
125
+@user_blueprint.route('/active')
126
+def active():
127
+    result = dict(status=False, data=[])
128
+    with database.get_db() as db:
129
+        with db.cursor() as cur:
130
+            cur.execute("SELECT * FROM recently_active_users ORDER BY posted DESC LIMIT 10")
131
+            store = result['data']
132
+            for id,name,last_posted in cur.fetchall():
133
+                store.append(
134
+                    dict(
135
+                        id=id,
136
+                        name=name,
137
+                        last_posted=last_posted.isoformat()
138
+                    )
139
+                )
140
+    return (json.dumps(result), 200, {'Content-Type': 'application/json'})
141
+
124 142
 @user_blueprint.route('/following')
125 143
 @login_required
126 144
 def following():
... ...
@@ -142,7 +160,7 @@ import os, base64
142 160
 def gen_ak(db):
143 161
     return ak
144 162
 
145
-@user_blueprint.route('/<user>/env', methods=['POST'])
163
+@user_blueprint.route('/env/<user>', methods=['POST'])
146 164
 def getenv(user): pass
147 165
 
148 166
 @user_blueprint.route('/change-password', methods=['POST'])
... ...
@@ -48,6 +48,10 @@ html {
48 48
 
49 49
 * {
50 50
   box-sizing: border-box;
51
+  -webkit-font-feature-settings: 'kern' 1, 'liga' 1;
52
+  -moz-font-feature-settings: 'kern' 1;
53
+  -o-font-feature-settings: 'kern' 1;
54
+  text-rendering: geometricPrecision;
51 55
 }
52 56
 
53 57
 body {
... ...
@@ -115,6 +119,8 @@ img {
115 119
 
116 120
 .identicon {
117 121
   border-radius: 50%;
122
+  /*border: thin solid #888;*/
123
+  box-shadow: 0em 0em 0.1em black;
118 124
   vertical-align: middle;
119 125
 }
120 126
 
... ...
@@ -134,9 +140,41 @@ img {
134 140
   width: 9em;
135 141
 }
136 142
 
143
+.filter-form {
144
+  padding-bottom: 0px;
145
+}
146
+
137 147
 ul.subscription-list {
138 148
   text-align: center;
139
-  /*background: #eee;*/
149
+  position: relative;
150
+  background: #888;
151
+  /*text-shadow: 0em 0em 0.1em black;*/
152
+  /*color: white;                    */
153
+  border: thin solid black;
154
+  padding: 0.75em 1.5em;
155
+  max-height: 1.5em;
156
+  overflow: hidden;
157
+  transition: max-height 1.5s ease, padding 1.5s ease;
158
+}
159
+
160
+ul.subscription-list.opened {
161
+  max-height: 12em;
162
+  padding-bottom: 2.5em;
163
+}
164
+
165
+ul.subscription-list > .list-control {
166
+  position: absolute;
167
+  width: 100%;
168
+  bottom: 0px;
169
+  left: 0px;
170
+  height: 1.5em;
171
+  background: #888;
172
+  color: white;
173
+  cursor: pointer;
174
+}
175
+ul.subscription-list > .list-control:hover {
176
+  background-color: #ccc;
177
+  /*color: black;*/
140 178
 }
141 179
 
142 180
 ul.subscription-list::after {
... ...
@@ -260,6 +298,11 @@ footer a.facebook-link:hover {
260 298
   margin-top: 10px;
261 299
 }
262 300
 
301
+#nav-main h3 {
302
+  text-align: right;
303
+  margin-top: 20px;
304
+}
305
+
263 306
 /* @end */
264 307
 
265 308
 /* @group List Module */
... ...
@@ -413,6 +456,19 @@ div.marrow {
413 456
   font-size: 80%;
414 457
   color: #aaa;
415 458
 }
459
+a.add-link {
460
+  color: green;
461
+  display: inline-block;
462
+  vertical-align: top;
463
+  position: relative;
464
+  top: 0.2em;
465
+  /*position: absolute;*/
466
+  /*margin-left: -1em;*/
467
+}
468
+a.add-link:hover {
469
+  text-decoration: none;
470
+  color: olive;
471
+}
416 472
 a.delete-link {
417 473
   color: red;
418 474
   display: inline-block;
419 475
Binary files a/static/images/icons/android-chrome-144x144.png and b/static/images/icons/android-chrome-144x144.png differ
420 476
Binary files a/static/images/icons/android-chrome-192x192.png and b/static/images/icons/android-chrome-192x192.png differ
421 477
Binary files a/static/images/icons/android-chrome-36x36.png and b/static/images/icons/android-chrome-36x36.png differ
422 478
Binary files a/static/images/icons/android-chrome-48x48.png and b/static/images/icons/android-chrome-48x48.png differ
423 479
Binary files a/static/images/icons/android-chrome-72x72.png and b/static/images/icons/android-chrome-72x72.png differ
424 480
Binary files a/static/images/icons/android-chrome-96x96.png and b/static/images/icons/android-chrome-96x96.png differ
425 481
Binary files a/static/images/icons/apple-touch-icon-114x114.png and b/static/images/icons/apple-touch-icon-114x114.png differ
426 482
Binary files a/static/images/icons/apple-touch-icon-120x120.png and b/static/images/icons/apple-touch-icon-120x120.png differ
427 483
Binary files a/static/images/icons/apple-touch-icon-144x144.png and b/static/images/icons/apple-touch-icon-144x144.png differ
428 484
Binary files a/static/images/icons/apple-touch-icon-152x152.png and b/static/images/icons/apple-touch-icon-152x152.png differ
429 485
Binary files a/static/images/icons/apple-touch-icon-180x180.png and b/static/images/icons/apple-touch-icon-180x180.png differ
430 486
Binary files a/static/images/icons/apple-touch-icon-57x57.png and b/static/images/icons/apple-touch-icon-57x57.png differ
431 487
Binary files a/static/images/icons/apple-touch-icon-60x60.png and b/static/images/icons/apple-touch-icon-60x60.png differ
432 488
Binary files a/static/images/icons/apple-touch-icon-72x72.png and b/static/images/icons/apple-touch-icon-72x72.png differ
433 489
Binary files a/static/images/icons/apple-touch-icon-76x76.png and b/static/images/icons/apple-touch-icon-76x76.png differ
434 490
Binary files a/static/images/icons/apple-touch-icon-precomposed.png and b/static/images/icons/apple-touch-icon-precomposed.png differ
435 491
Binary files a/static/images/icons/apple-touch-icon.png and b/static/images/icons/apple-touch-icon.png differ
... ...
@@ -2,11 +2,11 @@
2 2
 <browserconfig>
3 3
   <msapplication>
4 4
     <tile>
5
-      <square70x70logo src="/images/icons/mstile-70x70.png"/>
6
-      <square150x150logo src="/images/icons/mstile-150x150.png"/>
7
-      <square310x310logo src="/images/icons/mstile-310x310.png"/>
8
-      <wide310x150logo src="/images/icons/mstile-310x150.png"/>
9
-      <TileColor>#000000</TileColor>
5
+      <square70x70logo src="/images/icons/mstile-70x70.png?v=all2jv2L7Q"/>
6
+      <square150x150logo src="/images/icons/mstile-150x150.png?v=all2jv2L7Q"/>
7
+      <square310x310logo src="/images/icons/mstile-310x310.png?v=all2jv2L7Q"/>
8
+      <wide310x150logo src="/images/icons/mstile-310x150.png?v=all2jv2L7Q"/>
9
+      <TileColor>#da532c</TileColor>
10 10
     </tile>
11 11
   </msapplication>
12 12
 </browserconfig>
13 13
Binary files a/static/images/icons/favicon-16x16.png and b/static/images/icons/favicon-16x16.png differ
14 14
new file mode 100644
15 15
Binary files /dev/null and b/static/images/icons/favicon-194x194.png differ
16 16
Binary files a/static/images/icons/favicon-32x32.png and b/static/images/icons/favicon-32x32.png differ
17 17
Binary files a/static/images/icons/favicon-96x96.png and b/static/images/icons/favicon-96x96.png differ
18 18
Binary files a/static/images/icons/favicon.ico and b/static/images/icons/favicon.ico differ
... ...
@@ -2,40 +2,41 @@
2 2
 	"name": "Marrow",
3 3
 	"icons": [
4 4
 		{
5
-			"src": "\/images\/icons\/android-chrome-36x36.png",
5
+			"src": "\/images\/icons\/android-chrome-36x36.png?v=all2jv2L7Q",
6 6
 			"sizes": "36x36",
7 7
 			"type": "image\/png",
8 8
 			"density": "0.75"
9 9
 		},
10 10
 		{
11
-			"src": "\/images\/icons\/android-chrome-48x48.png",
11
+			"src": "\/images\/icons\/android-chrome-48x48.png?v=all2jv2L7Q",
12 12
 			"sizes": "48x48",
13 13
 			"type": "image\/png",
14 14
 			"density": "1.0"
15 15
 		},
16 16
 		{
17
-			"src": "\/images\/icons\/android-chrome-72x72.png",
17
+			"src": "\/images\/icons\/android-chrome-72x72.png?v=all2jv2L7Q",
18 18
 			"sizes": "72x72",
19 19
 			"type": "image\/png",
20 20
 			"density": "1.5"
21 21
 		},
22 22
 		{
23
-			"src": "\/images\/icons\/android-chrome-96x96.png",
23
+			"src": "\/images\/icons\/android-chrome-96x96.png?v=all2jv2L7Q",
24 24
 			"sizes": "96x96",
25 25
 			"type": "image\/png",
26 26
 			"density": "2.0"
27 27
 		},
28 28
 		{
29
-			"src": "\/images\/icons\/android-chrome-144x144.png",
29
+			"src": "\/images\/icons\/android-chrome-144x144.png?v=all2jv2L7Q",
30 30
 			"sizes": "144x144",
31 31
 			"type": "image\/png",
32 32
 			"density": "3.0"
33 33
 		},
34 34
 		{
35
-			"src": "\/images\/icons\/android-chrome-192x192.png",
35
+			"src": "\/images\/icons\/android-chrome-192x192.png?v=all2jv2L7Q",
36 36
 			"sizes": "192x192",
37 37
 			"type": "image\/png",
38 38
 			"density": "4.0"
39 39
 		}
40
-	]
40
+	],
41
+	"display": "standalone"
41 42
 }
42 43
Binary files a/static/images/icons/mstile-144x144.png and b/static/images/icons/mstile-144x144.png differ
43 44
Binary files a/static/images/icons/mstile-150x150.png and b/static/images/icons/mstile-150x150.png differ
44 45
Binary files a/static/images/icons/mstile-310x150.png and b/static/images/icons/mstile-310x150.png differ
45 46
Binary files a/static/images/icons/mstile-310x310.png and b/static/images/icons/mstile-310x310.png differ
46 47
Binary files a/static/images/icons/mstile-70x70.png and b/static/images/icons/mstile-70x70.png differ
47 48
new file mode 100644
... ...
@@ -0,0 +1,49 @@
1
+<?xml version="1.0" standalone="no"?>
2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
+ width="360.000000pt" height="337.000000pt" viewBox="0 0 360.000000 337.000000"
6
+ preserveAspectRatio="xMidYMid meet">
7
+<metadata>
8
+Created by potrace 1.11, written by Peter Selinger 2001-2013
9
+</metadata>
10
+<g transform="translate(0.000000,337.000000) scale(0.100000,-0.100000)"
11
+fill="#000000" stroke="none">
12
+<path d="M309 3354 c-21 -26 8 -65 75 -99 106 -54 97 57 94 -1162 -3 -1060 -3
13
+-1062 -24 -1090 -11 -15 -46 -40 -78 -55 -63 -31 -88 -66 -66 -93 11 -13 47
14
+-15 264 -13 l251 3 3 23 c5 32 -12 49 -81 83 -105 53 -97 -33 -97 1053 0 517
15
+2 937 6 934 3 -3 234 -487 514 -1074 l508 -1069 38 -3 39 -2 470 1017 c258
16
+560 491 1063 517 1118 l47 100 1 -991 0 -991 -23 -34 c-14 -21 -44 -44 -80
17
+-62 -64 -30 -89 -65 -67 -92 11 -13 58 -15 335 -15 351 0 356 1 336 53 -6 16
18
+-29 34 -64 51 -30 14 -67 40 -81 57 l-26 31 0 1066 c0 1225 -10 1106 100 1162
19
+67 34 92 68 70 95 -11 13 -48 15 -273 13 l-261 -3 -475 -1007 c-262 -554 -479
20
+-1004 -482 -1000 -4 4 -226 458 -495 1010 l-488 1002 -247 0 c-213 0 -248 -2
21
+-260 -16z"/>
22
+<path d="M158 461 c-144 -46 -196 -218 -100 -333 91 -108 265 -80 334 56 l15
23
+28 13 -33 c40 -102 161 -148 265 -100 52 24 74 50 95 112 l17 50 7 -33 c16
24
+-83 110 -158 196 -158 72 1 170 71 186 135 8 32 20 32 28 1 3 -14 18 -39 32
25
+-56 105 -124 308 -74 342 85 l8 40 7 -41 c10 -61 45 -105 106 -136 63 -32 110
26
+-35 166 -9 53 24 90 66 108 120 l14 44 13 -37 c16 -48 66 -105 107 -122 42
27
+-18 115 -18 156 0 42 18 82 61 102 110 l17 39 19 -44 c27 -58 55 -87 107 -110
28
+98 -43 203 -4 253 93 10 21 19 50 20 65 1 23 3 20 10 -13 12 -50 71 -121 116
29
+-140 41 -18 114 -18 156 0 38 15 94 76 103 111 8 33 20 32 32 -3 24 -70 109
30
+-131 182 -132 40 0 127 38 152 66 62 71 62 207 0 278 -25 28 -112 66 -152 66
31
+-73 -1 -158 -62 -182 -132 -12 -35 -24 -36 -32 -3 -9 35 -65 96 -103 111 -42
32
+18 -115 18 -156 0 -45 -19 -104 -90 -116 -140 -7 -33 -9 -36 -10 -13 -1 35
33
+-32 95 -63 124 -31 28 -93 53 -133 53 -43 0 -114 -31 -142 -61 -12 -13 -31
34
+-44 -42 -68 l-19 -44 -17 39 c-20 49 -60 92 -102 110 -41 18 -114 18 -156 0
35
+-41 -17 -91 -74 -107 -122 l-13 -37 -14 44 c-18 54 -55 96 -108 120 -56 26
36
+-103 23 -166 -8 -61 -32 -96 -76 -106 -137 l-7 -41 -8 40 c-34 159 -237 209
37
+-342 85 -14 -17 -29 -42 -32 -56 -8 -31 -20 -31 -28 1 -16 64 -114 134 -186
38
+135 -86 0 -180 -75 -196 -158 l-7 -33 -17 50 c-21 62 -43 88 -95 112 -100 46
39
+-221 4 -262 -91 l-15 -35 -20 40 c-11 22 -28 48 -37 59 -39 44 -139 74 -193
40
+57z m529 -79 c91 -65 79 -218 -21 -266 -105 -50 -216 21 -216 139 0 59 25 106
41
+72 136 43 26 122 22 165 -9z m800 0 c91 -65 79 -218 -21 -266 -49 -23 -107
42
+-20 -146 7 -56 37 -73 69 -73 132 0 63 17 95 74 133 43 30 120 27 166 -6z
43
+m796 2 c37 -27 67 -85 67 -129 0 -44 -30 -102 -67 -129 -15 -11 -48 -21 -77
44
+-24 -41 -3 -57 1 -85 20 -57 38 -75 72 -75 133 0 61 18 95 75 133 28 19 44 23
45
+85 20 29 -3 62 -13 77 -24z m787 4 c55 -38 75 -73 75 -133 0 -60 -19 -95 -76
46
+-133 -28 -19 -44 -23 -85 -20 -28 3 -62 13 -77 24 -32 23 -67 90 -67 127 0 57
47
+45 126 95 147 39 17 101 11 135 -12z"/>
48
+</g>
49
+</svg>
... ...
@@ -4,28 +4,31 @@
4 4
   <title>Marrow</title>
5 5
   <base href="/" />
6 6
   <meta charset="utf-8">
7
-  <meta name="msapplication-TileColor" content="#000000">
8
-  <meta name="msapplication-TileImage" content="/images/icons/mstile-144x144.png">
9
-  <meta name="msapplication-config" content="/images/icons/browserconfig.xml">
10
-  <meta name="theme-color" content="#000000">
11 7
   <meta name="viewport" content="width=device-width, initial-scale=1">
12 8
 
13 9
   <!-- Favicons/App Icons -->
14
-  <link rel="apple-touch-icon" sizes="57x57" href="/images/icons/apple-touch-icon-57x57.png">
15
-  <link rel="apple-touch-icon" sizes="60x60" href="/images/icons/apple-touch-icon-60x60.png">
16
-  <link rel="apple-touch-icon" sizes="72x72" href="/images/icons/apple-touch-icon-72x72.png">
17
-  <link rel="apple-touch-icon" sizes="76x76" href="/images/icons/apple-touch-icon-76x76.png">
18
-  <link rel="apple-touch-icon" sizes="114x114" href="/images/icons/apple-touch-icon-114x114.png">
19
-  <link rel="apple-touch-icon" sizes="120x120" href="/images/icons/apple-touch-icon-120x120.png">
20
-  <link rel="apple-touch-icon" sizes="144x144" href="/images/icons/apple-touch-icon-144x144.png">
21
-  <link rel="apple-touch-icon" sizes="152x152" href="/images/icons/apple-touch-icon-152x152.png">
22
-  <link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon-180x180.png">
23
-  <link rel="icon" type="image/png" href="/images/icons/favicon-32x32.png" sizes="32x32">
24
-  <link rel="icon" type="image/png" href="/images/icons/android-chrome-192x192.png" sizes="192x192">
25
-  <link rel="icon" type="image/png" href="/images/icons/favicon-96x96.png" sizes="96x96">
26
-  <link rel="icon" type="image/png" href="/images/icons/favicon-16x16.png" sizes="16x16">
27
-  <link rel="manifest" href="/images/icons/manifest.json">
28
-  <link rel="shortcut icon" href="/images/icons/favicon.ico">
10
+  <link rel="apple-touch-icon" sizes="57x57" href="/images/icons/apple-touch-icon-57x57.png?v=all2jv2L7Q">
11
+  <link rel="apple-touch-icon" sizes="60x60" href="/images/icons/apple-touch-icon-60x60.png?v=all2jv2L7Q">
12
+  <link rel="apple-touch-icon" sizes="72x72" href="/images/icons/apple-touch-icon-72x72.png?v=all2jv2L7Q">
13
+  <link rel="apple-touch-icon" sizes="76x76" href="/images/icons/apple-touch-icon-76x76.png?v=all2jv2L7Q">
14
+  <link rel="apple-touch-icon" sizes="114x114" href="/images/icons/apple-touch-icon-114x114.png?v=all2jv2L7Q">
15
+  <link rel="apple-touch-icon" sizes="120x120" href="/images/icons/apple-touch-icon-120x120.png?v=all2jv2L7Q">
16
+  <link rel="apple-touch-icon" sizes="144x144" href="/images/icons/apple-touch-icon-144x144.png?v=all2jv2L7Q">
17
+  <link rel="apple-touch-icon" sizes="152x152" href="/images/icons/apple-touch-icon-152x152.png?v=all2jv2L7Q">
18
+  <link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon-180x180.png?v=all2jv2L7Q">
19
+  <link rel="icon" type="image/png" href="/images/icons/favicon-32x32.png?v=all2jv2L7Q" sizes="32x32">
20
+  <link rel="icon" type="image/png" href="/images/icons/favicon-194x194.png?v=all2jv2L7Q" sizes="194x194">
21
+  <link rel="icon" type="image/png" href="/images/icons/favicon-96x96.png?v=all2jv2L7Q" sizes="96x96">
22
+  <link rel="icon" type="image/png" href="/images/icons/android-chrome-192x192.png?v=all2jv2L7Q" sizes="192x192">
23
+  <link rel="icon" type="image/png" href="/images/icons/favicon-16x16.png?v=all2jv2L7Q" sizes="16x16">
24
+  <link rel="manifest" href="/images/icons/manifest.json?v=all2jv2L7Q">
25
+  <link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#8eb7cf">
26
+  <link rel="shortcut icon" href="/images/icons/favicon.ico?v=all2jv2L7Q">
27
+  <meta name="msapplication-TileColor" content="#da532c">
28
+  <meta name="msapplication-TileImage" content="/images/icons/mstile-144x144.png?v=all2jv2L7Q">
29
+  <meta name="msapplication-config" content="/images/icons/browserconfig.xml?v=all2jv2L7Q">
30
+  <meta name="theme-color" content="#8eb7cf">
31
+
29 32
   <!-- JS -->
30 33
   <script src="//crypto-js.googlecode.com/svn/tags/3.0.2/build/rollups/md5.js"></script>
31 34
   <script src="/lib/angular.min.js"></script>
... ...
@@ -46,64 +49,70 @@
46 49
 
47 50
 </head>
48 51
 <body>
49
-<!--
50
-   -  <script>
51
-   -    window.fbAsyncInit = function() {FB.init({
52
-   -        appId      : '1420153671647757',
53
-   -        xfbml      : true,
54
-   -        version    : 'v2.3'
55
-   -      });
56
-   -    };
57
-   -
58
-   -    (function(d, s, id){
59
-   -      var js, fjs = d.getElementsByTagName(s)[0];
60
-   -      if (d.getElementById(id)) {return;}
61
-   -      js = d.createElement(s); js.id = id;
62
-   -      js.src = "//connect.facebook.net/en_US/sdk.js";
63
-   -      fjs.parentNode.insertBefore(js, fjs);
64
-   -    }(document, 'script', 'facebook-jssdk'));
65
-   -  </script>
66
-   -
67
-   -->
68
-  <div id="page-container">
69
-    <main ng-view></main>
70
-    <header>
71
-      <h1 class="site-logo">Marrow</h1>
72
-    </header>
73
-    <nav id="nav-main" ng-controller="SidebarCtrl">
74
-      <ul>
75
-        <li><a href="/">Home</a></li>
76
-        <li><a href ng-click="subscriptions()">My Lists</a></li>
77
-        <li><a href ng-click="random()">Random</a></li>
78
-        <li class="logout-link"><a href ng-click="logout()">Log Out</a></li>
79
-      </ul>
80
-      <!--
81
-         -<div class="appstore-links">
82
-         -  <a href="https://play.google.com/store/apps/details?id=com.joinmarrow.marrow">
83
-         -    <img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" />
84
-         -  </a>
85
-         -</div>
86
-         -->
87
-    </nav>
88
-    <footer>
89
-      <span class="beta-message">
90
-        This service is currently in beta. Like us on
91
-        <a class="facebook-link" href="https://www.facebook.com/join.marrow">Facebook.</a>
92
-        Try the
93
-        <a style="color: blue"
94
-           href="https://chrome.google.com/webstore/detail/add-to-marrow/pcgflajngpeopkemlijnnggfchoglpad">
95
-          Chrome Extension
96
-        </a>
97
-        to get the newest links delivered straight to your browser.
98
-        <!--or report problems on <a class="facebook-link" href="https://bugs.joinmarrow.com">bugs.joinmarrow.com</a>.-->
99
-      </span>
100
-    </footer>
101
-  </div>
102
-  <script>
103
-    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
104
-        (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
105
-        m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
106
-            })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
52
+  <!--
53
+    -  <script>
54
+-    window.fbAsyncInit = function() {FB.init({
55
+  -        appId      : '1420153671647757',
56
+  -        xfbml      : true,
57
+  -        version    : 'v2.3'
58
+    -      });
59
+-    };
60
+-
61
+-    (function(d, s, id){
62
+  -      var js, fjs = d.getElementsByTagName(s)[0];
63
+  -      if (d.getElementById(id)) {return;}
64
+  -      js = d.createElement(s); js.id = id;
65
+  -      js.src = "//connect.facebook.net/en_US/sdk.js";
66
+  -      fjs.parentNode.insertBefore(js, fjs);
67
+  -    }(document, 'script', 'facebook-jssdk'));
68
+-  </script>
69
+    -
70
+    -->
71
+    <div id="page-container">
72
+      <main ng-view></main>
73
+      <header>
74
+        <h1 class="site-logo">Marrow</h1>
75
+      </header>
76
+      <nav id="nav-main" ng-controller="SidebarCtrl">
77
+        <ul>
78
+          <li><a href="/">Home</a></li>
79
+          <li><a href ng-click="subscriptions()">My Lists</a></li>
80
+          <li><a href ng-click="random()">Random</a></li>
81
+          <li class="logout-link"><a href ng-click="logout()">Log Out</a></li>
82
+        </ul>
83
+        <h3>Active Users</h3>
84
+        <ul>
85
+          <li ng-repeat="user in activeUsers.data">
86
+            <user-badge poster="{{user.name}}" no-image></user-badge>
87
+          </li>
88
+        </ul>
89
+        <!--
90
+          -<div class="appstore-links">
91
+          -  <a href="https://play.google.com/store/apps/details?id=com.joinmarrow.marrow">
92
+          -    <img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" />
93
+          -  </a>
94
+          -</div>
95
+        -->
96
+      </nav>
97
+      <footer>
98
+        <span class="beta-message">
99
+          This service is currently in beta. Like us on
100
+          <a class="facebook-link" href="https://www.facebook.com/join.marrow">Facebook.</a>
101
+          Try the
102
+          <a style="color: blue"
103
+             href="https://chrome.google.com/webstore/detail/add-to-marrow/pcgflajngpeopkemlijnnggfchoglpad">
104
+            Chrome Extension
105
+          </a>
106
+          to get the newest links delivered straight to your browser.
107
+          <!--or report problems on <a class="facebook-link" href="https://bugs.joinmarrow.com">bugs.joinmarrow.com</a>.-->
108
+        </span>
109
+      </footer>
110
+    </div>
111
+    <script>
112
+(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
113
+  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
114
+  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
115
+})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
107 116
 
108 117
     ga('create', 'UA-61547817-1', 'auto');
109 118
       //ga('send', 'pageview');
... ...
@@ -7,6 +7,9 @@
7 7
       data-title="{{marrow.title}}"
8 8
       data-poster="{{marrow.poster}}"
9 9
       data-votes="{{marrow.votes}}">
10
+    <a href class="add-link" ng-click="reshare({item:marrow})" title="Reshare Link"
11
+      analytics-on="click" analytics-category="link" analytics-label="reshare"
12
+      analytics-event="{{marrow.url}}">{{!marrow.shared? '(+)': '&check;'}}</a>
10 13
     <span class="vote-disp">{{marrow.votes}}</span>
11 14
     <span ng-if="marrow.title">
12 15
       <a href="{{marrow.url}}" class="list-item" >{{marrow.title}}</a>
... ...
@@ -2,7 +2,7 @@ boneMod = angular.module('marrowApp.directives.boneList', []);
2 2
 
3 3
 boneMod.directive('boneList', function () {
4 4
   return {
5
-    scope: { bone: '=' },
5
+    scope: { bone: '=', reshare: '&' },
6 6
     templateUrl: 'js/directives/bone-list/bone-list.html'
7 7
   };
8 8
 });
... ...
@@ -1,4 +1,5 @@
1
-<a class="avatar-image" ng-href="/user/{{ poster }}" title="{{poster}}">
1
+<a ng-if="!withoutImage" class="avatar-image" ng-href="/user/{{ poster }}" title="{{poster}}">
2 2
   <gravatar-image class="poster-avatar" user-name="{{poster}}"></gravatar-image>
3
-  <span class="poster-handle de-emphasize">{{poster}}{{rep === undefined || rep === null? '' : ' ('+rep+')'}}</span>
3
+  <span class="poster-handle de-emphasize">{{poster}}</span>
4 4
 </a>
5
+<a ng-if="withoutImage" ng-href="/user/{{poster}}" title="{{poster}}">{{poster}}</a>
... ...
@@ -16,10 +16,12 @@ angular.module('marrowApp.directives.userBadge', ['marrowApp.utils'])
16 16
 .directive('userBadge', function() {
17 17
   return {
18 18
     scope: {
19
-      poster: '@', rep: '@',
19
+      poster: '@',
20
+      noImage: '@'
20 21
     },
21 22
     templateUrl: '/js/directives/user-badge/user-badge.html',
22
-    controller: function($scope) {
23
+    controller: function($scope,$attrs) {
24
+      $scope.withoutImage = $attrs.hasOwnProperty('noImage');
23 25
       $scope.gravURL = function(uid) {
24 26
         var hash = CryptoJS.MD5(uid);
25 27
         return '//gravatar.com/avatar/'+hash+'?d=identicon&s=24';
... ...
@@ -24,7 +24,7 @@ marrowApp.config(['$routeProvider',
24 24
         responseError: function(rejection) {
25 25
             if (rejection.status === 401) {
26 26
                 console.log("Response Error 401",rejection);
27
-                $window.location.href = '/login.html#' + encodeURIComponent($location.path());
27
+                $window.location.href = '/login.html';
28 28
             }
29 29
             return $q.reject(rejection);
30 30
         }
... ...
@@ -38,25 +38,23 @@ marrowApp.config(['$routeProvider',
38 38
 marrowApp.config(['$locationProvider', function($locationProvider) { $locationProvider.html5Mode(true); }]);
39 39
 
40 40
 //marrowApp.controller('LoginCtrl', function ($scope,$http,$route,$location) {
41
-//  $scope.tab = 'login';
42
-
43 41
 //  $scope.message = '';
44
-
42
+//
45 43
 //  var check_login = function () {
46
-//    var injector = angular.injector(['ng']);
47
-//    var $http = injector.get('$http');
44
+//    injector = angular.injector(['ng']);
45
+//    $http = injector.get('$http');
48 46
 //    return $http.get("/api/user/check").success(function(is_loggedon) {
49 47
 //      if (is_loggedon.result === true) {
50 48
 //        angular.element(document.body).addClass('is-logged-on');
51 49
 //      }
52 50
 //    });
53 51
 //  };
54
-
52
+//
55 53
 //  check_login().success(
56 54
 //    function(is_loggedon) {
57 55
 //      if (is_loggedon.result) { $location.url('/');}
58 56
 //  });
59
-
57
+//
60 58
 //  $scope.newuser = function () {
61 59
 //    var username = $scope.username;
62 60
 //    var password = $scope.password;
... ...
@@ -67,11 +65,11 @@ marrowApp.config(['$locationProvider', function($locationProvider) { $locationPr
67 65
 //      else {$scope.message = added_user.message;}
68 66
 //    });
69 67
 //  };
70
-
68
+//
71 69
 //  $scope.login = function () {
72 70
 //    var username = $scope.username;
73 71
 //    var password = $scope.password;
74
-
72
+//
75 73
 //    $http.post("/api/user/login", {"username":username, "password":password})
76 74
 //    .success(
77 75
 //      function (login_succeeded) {
... ...
@@ -82,10 +80,19 @@ marrowApp.config(['$locationProvider', function($locationProvider) { $locationPr
82 80
 //  };
83 81
 //});
84 82
 
85
-marrowApp.controller('RootCtrl', function ($scope,$http,$location,$route, SubscribedTo, BoneService, UserService) {
83
+marrowApp.controller('RootCtrl', function ($scope,$http,$location,$route, SubscribedTo, BoneService, UserService, $window) {
86 84
   $scope.url = "";
87 85
   $scope.title = "";
88 86
 
87
+  $scope.reshare = function (marrow) {
88
+    if (!marrow.shared ) {
89
+      $http.post('/api/bones/link/'+marrow.id).success(function(shared) {
90
+        marrow.shared = true;
91
+        if (shared === true) { $scope.update(); }
92
+      });
93
+    };
94
+  };
95
+
89 96
   $scope.toggleSubscribe = function (txt) {
90 97
     var postObj = {"from":$scope.bone.sectionTitle, "to":$scope.bone.sectionTitle};
91 98
     var promise = null;
... ...
@@ -266,7 +273,16 @@ marrowApp.controller('UserSettingCtrl', function ($scope,$http,$location) {
266 273
   };
267 274
 });
268 275
 
269
-marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route, $window) {
276
+marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route, $window, UserService) {
277
+  //eventSource = new EventSource("/api/user/active");
278
+  $scope.activeUsers = UserService.active();
279
+  //$scope.activeUsers = Object.create(null);
280
+  //$scope.activeUsers.users = []
281
+  //eventSource.addEventListener("active", function(event) {
282
+  //  var users = $scope.activeUsers.users;
283
+  //  Array.prototype.splice.apply(users, [0, users.length].concat(JSON.parse(event.data).data));
284
+  //});
285
+
270 286
   $scope.subscriptions = function() {
271 287
     if ($location.url() !== '/subscriptions') { $location.url('/subscriptions'); }
272 288
     else { $route.reload(); }
273 289
new file mode 100644
... ...
@@ -0,0 +1,322 @@
1
+window.URL = window.URL || window.webkitURL;
2
+var marrowApp = angular.module('marrowApp', ['ngRoute', 'marrowApp.services', 'marrowApp.directives', 'marrowApp.utils',
3
+                                             'marrowApp.directives.boneList', 'marrowApp.directives.userBadge',
4
+                                             'angulartics', 'angulartics.google.analytics', 'angulartics.piwik']);
5
+
6
+marrowApp.config(['$routeProvider',
7
+  function($routeProvider) {
8
+    $routeProvider.
9
+      when('/random', {templateUrl: 'partials/random.html', controller: 'RandomMarrowCtrl'}).
10
+      when('/settings', {templateUrl: 'partials/user-settings.html', controller: 'UserSettingCtrl'}).
11
+      when('/subscriptions', {templateUrl: 'partials/subscription.html', controller: 'SubscriptionCtrl'}).
12
+      when('/', {templateUrl: 'partials/default.html', controller: 'MarrowCtrl'}).
13
+      when('/user/:user', {template: '<div ng-include="templateUrl">Loading...</div>', controller: 'UserCtrl'});
14
+  }
15
+])
16
+.factory('authHttpResponseInterceptor',['$q','$location', '$window',function($q,$location,$window){
17
+    return {
18
+        response: function(response){
19
+            if (response.status === 401) {
20
+                console.log("Response 401");
21
+            }
22
+            return response || $q.when(response);
23
+        },
24
+        responseError: function(rejection) {
25
+            if (rejection.status === 401) {
26
+                console.log("Response Error 401",rejection);
27
+                $window.location.href = '/login.html#' + encodeURIComponent($location.path());
28
+            }
29
+            return $q.reject(rejection);
30
+        }
31
+    };
32
+}])
33
+.config(['$httpProvider',function($httpProvider) {
34
+    //Http Intercpetor to check auth failures for xhr requests
35
+    $httpProvider.interceptors.push('authHttpResponseInterceptor');
36
+}]);
37
+
38
+marrowApp.config(['$locationProvider', function($locationProvider) { $locationProvider.html5Mode(true); }]);
39
+
40
+//marrowApp.controller('LoginCtrl', function ($scope,$http,$route,$location) {
41
+<<<<<<< HEAD
42
+//  $scope.tab = 'login';
43
+
44
+//  $scope.message = '';
45
+
46
+//  var check_login = function () {
47
+//    var injector = angular.injector(['ng']);
48
+//    var $http = injector.get('$http');
49
+=======
50
+//  $scope.message = '';
51
+//
52
+//  var check_login = function () {
53
+//    injector = angular.injector(['ng']);
54
+//    $http = injector.get('$http');
55
+>>>>>>> Split login into its own html file
56
+//    return $http.get("/api/user/check").success(function(is_loggedon) {
57
+//      if (is_loggedon.result === true) {
58
+//        angular.element(document.body).addClass('is-logged-on');
59
+//      }
60
+//    });
61
+//  };
62
+<<<<<<< HEAD
63
+
64
+=======
65
+//
66
+>>>>>>> Split login into its own html file
67
+//  check_login().success(
68
+//    function(is_loggedon) {
69
+//      if (is_loggedon.result) { $location.url('/');}
70
+//  });
71
+<<<<<<< HEAD
72
+
73
+=======
74
+//
75
+>>>>>>> Split login into its own html file
76
+//  $scope.newuser = function () {
77
+//    var username = $scope.username;
78
+//    var password = $scope.password;
79
+//    var postObj = {"username":username, "password": password};
80
+//    $http.post("/api/user/add", postObj)
81
+//    .success(function(added_user) {
82
+//      if (added_user.status === true) {$location.url('/');}
83
+//      else {$scope.message = added_user.message;}
84
+//    });
85
+//  };
86
+<<<<<<< HEAD
87
+
88
+//  $scope.login = function () {
89
+//    var username = $scope.username;
90
+//    var password = $scope.password;
91
+
92
+=======
93
+//
94
+//  $scope.login = function () {
95
+//    var username = $scope.username;
96
+//    var password = $scope.password;
97
+//
98
+>>>>>>> Split login into its own html file
99
+//    $http.post("/api/user/login", {"username":username, "password":password})
100
+//    .success(
101
+//      function (login_succeeded) {
102
+//        var el = angular.element(document.querySelector('#login_form'));
103
+//        if (login_succeeded.status === true) {$location.url('/');}
104
+//        else {$scope.message = login_succeeded.message;}
105
+//    });
106
+//  };
107
+//});
108
+
109
+<<<<<<< HEAD
110
+marrowApp.controller('RootCtrl', function ($scope,$http,$location,$route, SubscribedTo, BoneService, UserService) {
111
+=======
112
+marrowApp.controller('RootCtrl', function ($scope,$http,$location,$route, SubscribedTo, BoneService, UserService, $window) {
113
+>>>>>>> Split login into its own html file
114
+  $scope.url = "";
115
+  $scope.title = "";
116
+
117
+  $scope.toggleSubscribe = function (txt) {
118
+    var postObj = {"from":$scope.bone.sectionTitle, "to":$scope.bone.sectionTitle};
119
+    var promise = null;
120
+
121
+    if ($scope.iFollow.follows) {
122
+      promise = $http.post('/api/bones/unsubscribe', postObj);
123
+    } else {
124
+      promise = $http.post('/api/bones/subscribe', postObj);
125
+    }
126
+
127
+    return promise.success(function(result) {
128
+      result = JSON.parse(result);
129
+      if (result) {
130
+        $scope.iFollow.follows = ! $scope.iFollow.follows;
131
+      }
132
+    });
133
+  };
134
+
135
+  $scope.bone = {sectionTitle: "", marrow: []};
136
+  $scope.friends = {data: []};
137
+
138
+  $scope.update = function() {
139
+    var config = {params: $scope.args? $scope.args: {}};
140
+    return $scope.getendpoint($scope.serviceParams, function(data) {
141
+      $scope.bone.sectionTitle = data.sectionTitle;
142
+      $scope.bone.marrow = data.marrow;
143
+      $scope.iFollow = UserService.follows({user:$scope.bone.sectionTitle});
144
+    }).$promise.then($scope._update);
145
+  };
146
+
147
+  UserService.check(function(is_loggedon) {
148
+    if (is_loggedon.result === true) {
149
+      angular.element(document.body).addClass('is-logged-on');
150
+    } else {
151
+<<<<<<< HEAD
152
+      //$window.location.href = '/login.html';
153
+=======
154
+      $window.location.href = '/login.html';
155
+>>>>>>> Split login into its own html file
156
+    }
157
+
158
+    $scope.update();
159
+  });
160
+
161
+});
162
+
163
+marrowApp.controller('RandomMarrowCtrl', function ($controller, $scope,$http,$location,$route, SubscribedTo, BoneService, UserService) {
164
+  $scope._update = function() {};
165
+  $scope.getendpoint = BoneService.random;
166
+  angular.extend(this, $controller('RootCtrl', {$scope: $scope}));
167
+});
168
+
169
+marrowApp.controller('SubscriptionCtrl', function ($controller,$scope,$http,$location,$route, SubscribedTo, BoneService, UserService) {
170
+  $scope.uncheckOthers = function (list) {
171
+    for (var n in list) {
172
+      if (n !== 'all' && list[n] === true) { list[n] = false; }
173
+    }
174
+  };
175
+
176
+  $scope.friend = Object.create(null);
177
+  $scope.friend.all = true;
178
+
179
+  $scope.upVote = function(boneItem) {
180
+    var apiCall = boneItem.myVote === 0? BoneService.vote_up: BoneService.vote_zero;
181
+    apiCall({url: boneItem.url}).$promise.then(function(r) {
182
+      if (r.success) {
183
+        boneItem.votes = r.votes;
184
+        boneItem.myVote = r.myVote;
185
+      }
186
+    }).then($scope._update);
187
+  };
188
+
189
+  $scope.backAPage = function() {
190
+    var bone = $scope.bone.marrow;
191
+    var lastitem = bone[bone.length-1].posted;
192
+    BoneService.subscriptions({before: lastitem}).$promise.then(function(r) {
193
+      while (r.marrow.length) {
194
+        $scope.bone.marrow.push(r.marrow.shift());
195
+      }
196
+    }).then($scope._update);
197
+  };
198
+
199
+  $scope.emptyOrEquals = function(actual, expected) {
200
+    var result = false;
201
+    if (!expected) { result = true; }
202
+    else if (expected.all) { result = true; }
203
+    else {result = expected[actual]; }
204
+    return result;
205
+  };
206
+
207
+  $scope.getBucket = function(date, buckets, classes) {
208
+    date = Date.parse(date);
209
+    var result = buckets.filter(function(x) {
210
+      return x >= date;
211
+    });
212
+    return classes[result[result.length-1]];
213
+  };
214
+
215
+  $scope.following_set = Object.create(null);
216
+  $scope._update = function() {
217
+    var marrow = $scope.bone.marrow;
218
+    var first = marrow[0].posted, last = marrow[marrow.length-1].posted;
219
+    first = Date.parse(first);
220
+    last = Date.parse(last);
221
+    var range = first - last;
222
+    console.log(range);
223
+    var bucketWidth = Math.ceil(range/20);
224
+    var buckets = [];
225
+    var bucketClasses= {};
226
+    for (var x = first; x > last; x -= bucketWidth) {
227
+      buckets.push(x);
228
+    }
229
+    for (var x = 0; x < 20; x++) { // jshint ignore:line
230
+      var bucket = x;
231
+      bucketClasses[buckets[bucket]] = 'bucket-'+bucket;
232
+    }
233
+    $scope.bone.marrow.map(function(o) {
234
+      o.colorClass = $scope.getBucket(o.posted, buckets, bucketClasses);
235
+      if (!(o.poster in $scope.following_set)) {
236
+        $scope.following_set[o.poster] = true;
237
+        $scope.friends.data.push(o.poster);
238
+      }
239
+    });
240
+    $scope.friends.reps = UserService.reputations($scope.friends.data);
241
+  };
242
+
243
+  $scope.getendpoint = BoneService.subscriptions;
244
+  angular.extend(this, $controller('RootCtrl', {$scope: $scope}));
245
+});
246
+
247
+marrowApp.controller('MarrowCtrl', function ($controller,$scope,$http,$location,$route, SubscribedTo, BoneService, UserService) {
248
+  $scope.postobj = {url: "", title: ""};
249
+
250
+  $scope.delete = function (linkid) {
251
+    $http.delete('/api/bones/link/'+linkid).success(function (deleted) {
252
+      deleted = JSON.parse(deleted);
253
+      if (deleted === true) { $scope.update(); }
254
+    });
255
+  };
256
+
257
+  $scope.addLink = function() {
258
+    $http.post('/api/bones/add', $scope.postobj).success(function(data) {
259
+      if (data.success) {
260
+        $scope.postobj.url = "";
261
+        $scope.update();
262
+      }
263
+    });
264
+  };
265
+
266
+  if ($scope.getendpoint === undefined) {
267
+    $scope.getendpoint = BoneService.get;
268
+  }
269
+  angular.extend(this, $controller('RootCtrl', {$scope: $scope}));
270
+});
271
+
272
+marrowApp.controller('UserCtrl', function ($controller, $scope,$http,$routeParams, UserService, BoneService) {
273
+  var user = $routeParams.user;
274
+  $scope.getendpoint = BoneService.user;
275
+  $scope.serviceParams = {user: user};
276
+
277
+  angular.extend(this, $controller('MarrowCtrl', {$scope: $scope}));
278
+  $scope._update = function() {
279
+    $scope.iFollow.$promise.then(function(result) {
280
+      $scope.templateUrl = result.me === user? "/partials/default.html": "/partials/random.html";
281
+    });
282
+  };
283
+
284
+});
285
+
286
+marrowApp.controller('UserSettingCtrl', function ($scope,$http,$location) {
287
+  $scope.oldPassword = '';
288
+  $scope.newPassword = '';
289
+  $scope.changePassword = function() {
290
+    var postObj = {"old_password": $scope.oldPassword, "new_password": $scope.newPassword};
291
+    $http.post('/api/user/change-password', postObj).success(function(result) {
292
+      if (result.status === true) {
293
+        $location.url('/');
294
+      } else {
295
+        $scope.message = result.message;
296
+      }
297
+    });
298
+  };
299
+});
300
+
301
+marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route, $window) {
302
+  $scope.subscriptions = function() {
303
+    if ($location.url() !== '/subscriptions') { $location.url('/subscriptions'); }
304
+    else { $route.reload(); }
305
+  };
306
+
307
+  $scope.random = function() {
308
+    if ($location.url() !== '/random') { $location.url('/random'); }
309
+    else { $route.reload(); }
310
+  };
311
+
312
+  $scope.logout = function() {
313
+    $http.get('/api/user/logout').success(function() {
314
+<<<<<<< HEAD
315
+      $window.location.href = '/';
316
+=======
317
+      $window.location.href = '/login.html';
318
+>>>>>>> Split login into its own html file
319
+    });
320
+  };
321
+});
322
+
... ...
@@ -1,5 +1,5 @@
1 1
 window.URL = window.URL || window.webkitURL;
2
-var loginModule = angular.module('marrowLogin', ['ngResource','ngRoute','angulartics', 'angulartics.google.analytics']);
2
+var loginModule = angular.module('marrowLogin', ['ngResource','ngRoute','angulartics', 'angulartics.google.analytics', 'angulartics.piwik']);
3 3
 
4 4
 loginModule.controller('LoginCtrl', function ($scope,$http,$route,$window) {
5 5
   $scope.message = '';
... ...
@@ -14,10 +14,10 @@ loginModule.controller('LoginCtrl', function ($scope,$http,$route,$window) {
14 14
     });
15 15
   };
16 16
 
17
-  check_login().success(
18
-    function(is_loggedon) {
19
-      if (is_loggedon.result) { $window.location.href = '/';}
20
-  });
17
+  //check_login().success(
18
+  //  function(is_loggedon) {
19
+  //    if (is_loggedon.result) { $window.location.href = '/';}
20
+  //});
21 21
 
22 22
   $scope.newuser = function () {
23 23
     var username = $scope.username;
24 24
new file mode 100644
... ...
@@ -0,0 +1,49 @@
1
+<<<<<<< HEAD
2
+window.URL = window.URL || window.webkitURL;
3
+var loginModule = angular.module('marrowLogin', ['ngResource','ngRoute','angulartics', 'angulartics.google.analytics']);
4
+=======
5
+var loginModule = angular.module('marrowApp.login', ['ngResource','ngRoute','angulartics', 'angulartics.google.analytics']);
6
+>>>>>>> Split login into its own html file
7
+
8
+loginModule.controller('LoginCtrl', function ($scope,$http,$route,$window) {
9
+  $scope.message = '';
10
+
11
+  var check_login = function () {
12
+    injector = angular.injector(['ng']);
13
+    $http = injector.get('$http');
14
+    return $http.get("/api/user/check").success(function(is_loggedon) {
15
+      if (is_loggedon.result === true) {
16
+        angular.element(document.body).addClass('is-logged-on');
17
+      }
18
+    });
19
+  };
20
+
21
+  check_login().success(
22
+    function(is_loggedon) {
23
+      if (is_loggedon.result) { $window.location.href = '/';}
24
+  });
25
+
26
+  $scope.newuser = function () {
27
+    var username = $scope.username;
28
+    var password = $scope.password;
29
+    var postObj = {"username":username, "password": password};
30
+    $http.post("/api/user/add", postObj)
31
+    .success(function(added_user) {
32
+      if (added_user.status === true) {$window.location.href = '/';}
33
+      else {$scope.message = added_user.message;}
34
+    });
35
+  };
36
+
37
+  $scope.login = function () {
38
+    var username = $scope.username;
39
+    var password = $scope.password;
40
+
41
+    $http.post("/api/user/login", {"username":username, "password":password})
42
+    .success(
43
+      function (login_succeeded) {
44
+        var el = angular.element(document.querySelector('#login_form'));
45
+        if (login_succeeded.status === true) {$window.location.href = '/';}
46
+        else {$scope.message = login_succeeded.message;}
47
+    });
48
+  };
49
+});
... ...
@@ -38,7 +38,8 @@ serviceModule.factory('UserService', ['$resource',
38 38
       environment: {'method': 'POST', 'url': '/api/user/environment'},
39 39
       reputation: {'method': 'POST', 'url': '/api/user/reputation/:user', params: {user: '@user'}},
40 40
       reputations: {'method': 'POST', 'url': '/api/user/reputation'},
41
-      changePassword: {'method': 'POST', 'url': '/api/user/change-password'}
41
+      changePassword: {'method': 'POST', 'url': '/api/user/change-password'},
42
+      active: {'method': 'GET', 'url': '/api/user/active'}
42 43
     });
43 44
   }]
44 45
 );
... ...
@@ -32,6 +32,7 @@
32 32
   <script src="/lib/angular-resource.min.js"></script>
33 33
   <script src="/lib/angulartics.min.js"></script>
34 34
   <script src="/lib/angulartics-ga.min.js"></script>
35
+  <script src="/lib/angulartics-piwik.js"></script>
35 36
   <script src="/js/new/login.js"></script>
36 37
   <!-- CSS -->
37 38
   <link rel="stylesheet" href="/lib/formalize.css" media="screen" />
... ...
@@ -48,8 +49,8 @@
48 49
       <div class="message">{{message}}</div>
49 50
       <input type="text" ng-model="username" placeholder="Username" />
50 51
       <input type="password" ng-model="password" placeholder="Password" />
51
-      <button ng-click="login()">Log In</button>
52
-      <button ng-click="newuser()">Add User</button>
52
+      <button ng-click="login()" analytics-on="click" analytics-category="user" analytics-event="login">Log In</button>
53
+      <button ng-click="newuser()" analytics-on="click" analytics-category="user" analytics-event="new" analytics-label="{{username}}">Add User</button>
53 54
     </form>
54 55
   </div>
55 56
   </main>
56 57
new file mode 100644
... ...
@@ -0,0 +1,75 @@
1
+<html lang="en">
2
+<head>
3
+  <title>Marrow</title>
4
+  <base href="/login.html" />
5
+  <meta charset="utf-8">
6
+  <meta name="msapplication-TileColor" content="#000000">
7
+  <meta name="msapplication-TileImage" content="/images/icons/mstile-144x144.png">
8
+  <meta name="msapplication-config" content="/images/icons/browserconfig.xml">
9
+  <meta name="theme-color" content="#000000">
10
+  <meta name="viewport" content="width=device-width, initial-scale=1">
11
+
12
+  <!-- Favicons/App Icons -->
13
+  <link rel="apple-touch-icon" sizes="57x57" href="/images/icons/apple-touch-icon-57x57.png">
14
+  <link rel="apple-touch-icon" sizes="60x60" href="/images/icons/apple-touch-icon-60x60.png">
15
+  <link rel="apple-touch-icon" sizes="72x72" href="/images/icons/apple-touch-icon-72x72.png">
16
+  <link rel="apple-touch-icon" sizes="76x76" href="/images/icons/apple-touch-icon-76x76.png">
17
+  <link rel="apple-touch-icon" sizes="114x114" href="/images/icons/apple-touch-icon-114x114.png">
18
+  <link rel="apple-touch-icon" sizes="120x120" href="/images/icons/apple-touch-icon-120x120.png">
19
+  <link rel="apple-touch-icon" sizes="144x144" href="/images/icons/apple-touch-icon-144x144.png">
20
+  <link rel="apple-touch-icon" sizes="152x152" href="/images/icons/apple-touch-icon-152x152.png">
21
+  <link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon-180x180.png">
22
+  <link rel="icon" type="image/png" href="/images/icons/favicon-32x32.png" sizes="32x32">
23
+  <link rel="icon" type="image/png" href="/images/icons/android-chrome-192x192.png" sizes="192x192">
24
+  <link rel="icon" type="image/png" href="/images/icons/favicon-96x96.png" sizes="96x96">
25
+  <link rel="icon" type="image/png" href="/images/icons/favicon-16x16.png" sizes="16x16">
26
+  <link rel="manifest" href="/images/icons/manifest.json">
27
+  <link rel="shortcut icon" href="/images/icons/favicon.ico">
28
+  <!-- JS -->
29
+  <script src="//crypto-js.googlecode.com/svn/tags/3.0.2/build/rollups/md5.js"></script>
30
+  <script src="/lib/angular.min.js"></script>
31
+  <script src="/lib/angular-route.min.js"></script>
32
+  <script src="/lib/angular-resource.min.js"></script>
33
+  <script src="/lib/angulartics.min.js"></script>
34
+  <script src="/lib/angulartics-ga.min.js"></script>
35
+  <script src="/js/new/login.js"></script>
36
+  <!-- CSS -->
37
+  <link rel="stylesheet" href="/lib/formalize.css" media="screen" />
38
+  <link rel="stylesheet" href="/css/main.css" media="screen" />
39
+</head>
40
+
41
+<body ng-app="marrowLogin">
42
+  <header>
43
+    <h1 class="site-logo">Marrow</h1>
44
+  </header>
45
+  <main ng-controller="LoginCtrl">
46
+  <div id="login_form">
47
+    <form>
48
+      <div class="message">{{message}}</div>
49
+      <input type="text" ng-model="username" placeholder="Username" />
50
+      <input type="password" ng-model="password" placeholder="Password" />
51
+      <button ng-click="login()">Log In</button>
52
+      <button ng-click="newuser()">Add User</button>
53
+    </form>
54
+  </div>
55
+  </main>
56
+<!-- Piwik -->
57
+<script type="text/javascript">
58
+  var _paq = _paq || [];
59
+  _paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
60
+  _paq.push(["setCookieDomain", "*.joinmarrow.com"]);
61
+  _paq.push(["setDomains", ["*.joinmarrow.com"]]);
62
+  _paq.push(['trackPageView']);
63
+  _paq.push(['enableLinkTracking']);
64
+  (function() {
65
+    var u="//piwik.elangley.org/";
66
+    _paq.push(['setTrackerUrl', u+'piwik.php']);
67
+    _paq.push(['setSiteId', 2]);
68
+    var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
69
+    g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
70
+  })();
71
+</script>
72
+<noscript><p><img src="//piwik.elangley.org/piwik.php?idsite=2" style="border:0;" alt="" /></p></noscript>
73
+<!-- End Piwik Code -->
74
+</body>
75
+</html>
... ...
@@ -12,7 +12,7 @@
12 12
   </h2>
13 13
   <form>
14 14
     <input type="text" ng-model="postobj.url" placeholder="http:// . . ."/>
15
-    <button type="submit" ng-click="addLink(postobj.url)">+</button>
15
+    <button type="submit" ng-click="addLink(postobj.url)" analytics-on="click" analytics-category="link" analytics-label="add" analytics-event="{{postobj.url}}">+</button>
16 16
   </form>
17 17
   <ul class="bone-list">
18 18
     <li ng-repeat="marrow in bone.marrow"
... ...
@@ -22,7 +22,9 @@
22 22
         data-posted="{{marrow.posted}}"
23 23
         data-title="{{marrow.title}}"
24 24
         data-votes="{{marrow.votes}}">
25
-      <a href class="delete-link" ng-click="delete(marrow.id)" title="Delete Link from List">(-)</a>
25
+      <a href class="delete-link" ng-click="delete(marrow.id)" title="Delete Link from List"
26
+        analytics-on="click" analytics-category="link" analytics-label="delete"
27
+        analytics-event="{{marrow.url}}">(-)</a>
26 28
       <div class="marrow">
27 29
         <span class="vote-disp">{{marrow.votes}}</span>
28 30
         <span ng-if="marrow.title">
... ...
@@ -13,7 +13,7 @@
13 13
       <gravatar-image class="my-image" user-name="{{bone.sectionTitle}}"></gravatar-image>
14 14
     </a>
15 15
   </h2>
16
-  <bone-list bone="bone.marrow" />
16
+  <bone-list bone="bone.marrow" reshare="reshare(item)" />
17 17
 </section>
18 18
 
19 19
 
... ...
@@ -2,8 +2,8 @@
2 2
   <h2 class="section-title">
3 3
     {{bone.sectionTitle}} ({{filtered.length}} items)
4 4
   </h2>
5
-  <form>
6
-    <ul class="subscription-list">
5
+  <form class="filter-form">
6
+    <ul ng-class="{'subscription-list': true, 'opened': opened}">
7 7
       <li class="sub-filter">
8 8
         <input id="sub-all" type="checkbox" ng-model="friend.all" ng-click="uncheckOthers(friend)"></input>
9 9
         <label for="sub-all">[All]</label>
... ...
@@ -15,6 +15,10 @@
15 15
           <span class="wide">{{name}}</span>
16 16
         </label>
17 17
       </li>
18
+      <div class="list-control" ng-click="opened = ! opened">
19
+        <span ng-if="opened">Hide . . .</span>
20
+        <span ng-if="!opened">Filter . . .</span>
21
+      </div>
18 22
     </ul>
19 23
   </form>
20 24
   <ul class="bone-list">
... ...
@@ -30,15 +34,20 @@
30 34
         <span class="voting">
31 35
           <span class="score">{{marrow.votes}}</span>
32 36
           <button class="upVote vote-button fa fa-plus" ng-class="{selected: marrow.myVote===1}" ng-click="upVote(marrow)"
33
-                  analytics-on="click" analytics-event="vote" analytics-category="{{marrow.myVote===0? 'up' : 'zero'}}"></button>
37
+                  analytics-on="click" analytics-event="vote" analytics-category="{{marrow.myVote===0? 'up' : 'zero'}}">
38
+          </button>
34 39
         </span>
35 40
         <span ng-if="marrow.title">
36 41
           <a href="{{marrow.url}}" class="list-item">{{marrow.title}}</a>
37 42
           <br />
38 43
         </span>
44
+        <a class="add-link" ng-click="reshare(marrow)" title="Reshare Link"
45
+           analytics-on="click" analytics-category="link" analytics-label="reshare"
46
+           analytics-event="{{marrow.url}}">{{!marrow.shared? 'Reshare': '&check;'}}</a>
47
+        &mdash;
39 48
         <a href="{{marrow.url}}" ng-class="{'de-emphasize':marrow.title}" >{{marrow.url}}</a>
40 49
       </div>
41
-      <user-badge poster="{{marrow.poster}}" rep="{{friends.reps.reputation[marrow.poster]}}"></user-badge>
50
+      <user-badge poster="{{marrow.poster}}"></user-badge>
42 51
     </li>
43 52
   </ul>
44 53
   <a ng-click="backAPage()" class="more-link"/>More</a>