Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Star toggling

  • Loading branch information...
commit 4cf4dbd26080ec546bd35d43474995bddde1e800 1 parent ff872e5
Matt Jibson authored
6 index.yaml
View
@@ -15,3 +15,9 @@ indexes:
properties:
- name: c
direction: desc
+
+- kind: US
+ ancestor: yes
+ properties:
+ - name: c
+ direction: desc
1  main.go
View
@@ -71,6 +71,7 @@ func init() {
router.Handle("/user/mark-read", mpg.NewHandler(MarkRead)).Name("mark-read")
router.Handle("/user/mark-unread", mpg.NewHandler(MarkUnread)).Name("mark-unread")
router.Handle("/user/save-options", mpg.NewHandler(SaveOptions)).Name("save-options")
+ router.Handle("/user/set-star", mpg.NewHandler(SetStar)).Name("set-star")
router.Handle("/user/upload-opml", mpg.NewHandler(UploadOpml)).Name("upload-opml")
router.Handle("/admin/all-feeds", mpg.NewHandler(AllFeeds)).Name("all-feeds")
21 static/js/site.js
View
@@ -112,6 +112,7 @@ goReadAppModule.controller('GoreadCtrl', function($scope, $http, $timeout, $wind
$scope.xmlurls = {};
$scope.icons = {};
$scope.feedData = {};
+ $scope.stars = {};
};
$scope.update = function() {
@@ -160,6 +161,9 @@ goReadAppModule.controller('GoreadCtrl', function($scope, $http, $timeout, $wind
e.NextUpdate = moment(e.NextUpdate).fromNow();
$scope.feedData[e.Url] = e;
});
+ _.each(data.Stars, function(s) {
+ $scope.stars[s] = true;
+ });
$scope.opts = data.Options ? JSON.parse(data.Options) : $scope.opts;
$scope.trialRemaining = data.TrialRemaining;
@@ -751,7 +755,13 @@ goReadAppModule.controller('GoreadCtrl', function($scope, $http, $timeout, $wind
f: f,
c: $scope.cursors[f] || ''
})).success(function (data) {
- if (!data || !data.Stories) return
+ if (!data) return;
+ if (data.Stars) {
+ _.each(data.Stars, function(s) {
+ $scope.stars[s] = true;
+ });
+ }
+ if (!data.Stories) return;
delete $scope.fetching[f]
$scope.cursors[$scope.activeFeed] = data.Cursor;
if (!$scope.readStories[f])
@@ -972,6 +982,15 @@ goReadAppModule.controller('GoreadCtrl', function($scope, $http, $timeout, $wind
});
};
+ $scope.toggleStar = function(story) {
+ $scope.stars[story.guid] = !$scope.stars[story.guid];
+ $scope.http('POST', $('#mark-all-read').attr('data-url-star'), {
+ feed: story.feed.XmlUrl,
+ story: story.Id,
+ del: $scope.stars[story.guid] ? '' : '1'
+ });
+ };
+
$scope.encode = encodeURIComponent;
$scope.shortcuts = $('#shortcuts');
16 templates/base.html
View
@@ -69,13 +69,21 @@
.story-header.read {
background-color: #f5f5f5;
}
+ .story-star {
+ position: absolute;
+ left: 5px;
+ color: rgba(0, 0, 0, 0.4);
+ }
+ .story-star:hover {
+ color: rgba(0, 0, 0, 1);
+ }
.story-feed {
- width: 145px;
+ width: 130px;
overflow: hidden;
text-overflow: ellipsis;
position: absolute;
white-space: nowrap;
- left: 5px;
+ left: 20px;
}
.story-date {
position: absolute;
@@ -239,6 +247,7 @@
data-url-read="{{url "mark-read"}}"
data-url-unread="{{url "mark-unread"}}"
data-url-contents="{{url "get-contents"}}"
+ data-url-star="{{url "set-star"}}"
>mark all read</button>
<button
id="refresh"
@@ -616,6 +625,9 @@
ng-class="{selected: $index == currentStory}"
>
<div class="story-header hand" ng-class="{read: s.read}" ng-click="setCurrent($index, {collapse: 'toggle'})">
+ <div class="story-star" ng-click="toggleStar(s); $event.stopPropagation()">
+ <i ng-class="stars[s.guid] ? 'icon-star' : 'icon-star-empty'"></i>
+ </div>
<div class="story-feed muted hidden-xs" ng-bind="s.feed.Title"></div>
<div class="story-title" title="{{`{{s.Title}}`}}" dir="auto">
<a ng-href="{{`{{s.Link}}`}}" ng-click="setCurrent($index, null, $event); $event.stopPropagation()" class="story-link"><strong ng-bind="s.Title"></strong></a>
32 types.go
View
@@ -20,6 +20,7 @@ import (
"bytes"
"compress/gzip"
"encoding/base64"
+ "fmt"
"io/ioutil"
"net/url"
"time"
@@ -27,6 +28,8 @@ import (
"appengine"
"appengine/datastore"
"appengine/taskqueue"
+ "appengine/user"
+ "github.com/mjibson/goon"
)
type User struct {
@@ -81,6 +84,35 @@ func (uo *UserOpml) opml() []byte {
return uo.Opml
}
+type UserStarFeed struct {
+ _kind string `goon:"kind,USF"`
+ Id string `datastore:"-" goon:"id"`
+ Parent *datastore.Key `datastore:"-" goon:"parent"`
+}
+
+// parent: UserStarFeed, key: Story.Key.Encode()
+type UserStar struct {
+ _kind string `goon:"kind,US"`
+ Id string `datastore:"-" goon:"id"`
+ Parent *datastore.Key `datastore:"-" goon:"parent"`
+ Created time.Time `datastore:"c"`
+}
+
+func starKey(c appengine.Context, feed, story string) *UserStar {
+ cu := user.Current(c)
+ gn := goon.FromContext(c)
+ u := User{Id: cu.ID}
+ uk := gn.Key(&u)
+ return &UserStar{
+ Parent: datastore.NewKey(c, "USF", feed, 0, uk),
+ Id: story,
+ }
+}
+
+func starID(key *datastore.Key) string {
+ return fmt.Sprintf("%s|%s", key.Parent().StringID(), key.StringID())
+}
+
type readStory struct {
Feed, Story string
}
61 user.go
View
@@ -246,6 +246,7 @@ func ListFeeds(c mpg.Context, w http.ResponseWriter, r *http.Request) {
updatedLinks := false
now := time.Now()
numStories := 0
+ var stars []string
c.Step(fmt.Sprintf("feed unreads: %v", u.Read), func(c mpg.Context) {
queue := make(chan *Feed)
@@ -315,6 +316,20 @@ func ListFeeds(c mpg.Context, w http.ResponseWriter, r *http.Request) {
queue <- f
}
close(queue)
+ c.Step("stars", func(c mpg.Context) {
+ gn := goon.FromContext(c)
+ q := datastore.NewQuery(gn.Key(&UserStar{}).Kind()).
+ Ancestor(ud.Parent).
+ KeysOnly().
+ Filter("c >=", u.Read).
+ Order("-c")
+ keys, _ := gn.GetAll(q, nil)
+ stars = make([]string, len(keys))
+ for i, key := range keys {
+ stars[i] = starID(key)
+ }
+ c.Infof("star keys: %v", keys)
+ })
// wait for feeds to complete so there are no more tasks to queue
wg.Wait()
// then finish enqueuing tasks
@@ -424,12 +439,14 @@ func ListFeeds(c mpg.Context, w http.ResponseWriter, r *http.Request) {
Options string
TrialRemaining int
Feeds []*Feed
+ Stars []string
}{
Opml: uf.Outline,
Stories: fl,
Options: u.Options,
TrialRemaining: trialRemaining,
Feeds: feeds,
+ Stars: stars,
}
b, err := json.Marshal(o)
if err != nil {
@@ -733,13 +750,29 @@ func SaveOptions(c mpg.Context, w http.ResponseWriter, r *http.Request) {
func GetFeed(c mpg.Context, w http.ResponseWriter, r *http.Request) {
gn := goon.FromContext(c)
f := Feed{Url: r.FormValue("f")}
+ var stars []string
+ wg := sync.WaitGroup{}
fk := gn.Key(&f)
q := datastore.NewQuery(gn.Key(&Story{}).Kind()).Ancestor(fk).KeysOnly()
q = q.Order("-" + IDX_COL)
- if c := r.FormValue("c"); c != "" {
- if dc, err := datastore.DecodeCursor(c); err == nil {
+ if cur := r.FormValue("c"); cur != "" {
+ if dc, err := datastore.DecodeCursor(cur); err == nil {
q = q.Start(dc)
}
+ } else {
+ // grab the stars list on the first run
+ wg.Add(1)
+ go c.Step("stars", func(c mpg.Context) {
+ gn := goon.FromContext(c)
+ usk := starKey(c, f.Url, "")
+ q := datastore.NewQuery(gn.Key(&UserStar{}).Kind()).Ancestor(gn.Key(usk).Parent()).KeysOnly()
+ keys, _ := gn.GetAll(q, nil)
+ stars = make([]string, len(keys))
+ for i, key := range keys {
+ stars[i] = starID(key)
+ }
+ wg.Done()
+ })
}
iter := gn.Run(q)
var stories []*Story
@@ -761,12 +794,15 @@ func GetFeed(c mpg.Context, w http.ResponseWriter, r *http.Request) {
cursor = ic.String()
}
gn.GetMulti(&stories)
+ wg.Wait()
b, _ := json.Marshal(struct {
Cursor string
Stories []*Story
+ Stars []string `json:",omitempty"`
}{
Cursor: cursor,
Stories: stories,
+ Stars: stars,
})
w.Write(b)
}
@@ -787,3 +823,24 @@ func DeleteAccount(c mpg.Context, w http.ResponseWriter, r *http.Request) {
gn.Delete(ud.Parent)
http.Redirect(w, r, routeUrl("logout"), http.StatusFound)
}
+
+func SetStar(c mpg.Context, w http.ResponseWriter, r *http.Request) {
+ feed := r.FormValue("feed")
+ story := r.FormValue("story")
+ if len(feed) == 0 || len(story) == 0 {
+ return
+ }
+ del := r.FormValue("del") != ""
+ us := starKey(c, feed, story)
+ gn := goon.FromContext(c)
+ if del {
+ gn.Delete(gn.Key(us))
+ } else {
+ us.Created = time.Now()
+ _, err := gn.Put(us)
+ if err != nil {
+ c.Errorf("star put err: %v", err)
+ serveError(w, err)
+ }
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.