vickychijwani / quill Goto Github PK
View Code? Open in Web Editor NEW:ghost: [MOVED TO https://github.com/TryGhost/Ghost-Android] The beautiful Android app for your Ghost blog.
License: MIT License
:ghost: [MOVED TO https://github.com/TryGhost/Ghost-Android] The beautiful Android app for your Ghost blog.
License: MIT License
Hello,
First of all i want to congratulate you! Great app ! Is there a documentation for installing it localy or use it for my ghost blog?
Thanks!
This issue belongs to the create / edit post epic: #5.
Hawk looks like an attractive option for this. ("Secure simple key-value storage")
This is an overarching "epic" issue for the login workflow. The checklist links to individual issues.
Description: As a user, I want to be able to login to my Ghost blog.
Although Ghost doesn't have an official public API yet, it is possible to use the internal API by masquerading as the Ghost web client. I realize that it could change at any time without any warning whatsoever, but the hope is that it probably won't. That way when the official OAuth API is released it'll be easier to port this app (hopefully just an API version change and a few minor tweaks).
A typical login request / response in Ghost 0.5.7 looks like this (abbreviated):
Request (when fetching access token using a password):
POST /ghost/api/v0.1/authentication/token HTTP/1.1
Host: blog.example.com
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=<username>&password=<password>&client_id=ghost-admin
Request (when fetching access token using a refresh token):
POST /ghost/api/v0.1/authentication/token HTTP/1.1
Host: blog.example.com
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<refresh_token>&client_id=ghost-admin
Response:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "<access_token>",
"refresh_token": "<refresh_token>", // absent when requested grant_type is "refresh_token"
"expires_in": 3600,
"token_type":"Bearer"
}
Checklist:
This issue belongs to the create / edit post epic: #5.
Experimentation with the /posts
API reveals that it is very forgiving in general :). For example, the following short request will successfully create a new draft (of these parameters, only title
and markdown
are required):
POST /posts/
Content-Type: application/json; charset=utf-8
Authorization: Bearer <access_token>
{
"posts": [{
"title": "(Untitled)",
"markdown": "",
"status": "draft"
}]
}
201 Created
{
"posts": [{
"id": 27,
"uuid": "5f04e432-67e4-48be-a3b8-0a3beb3d5b0c",
"title": "(Untitled)",
"slug": "untitled-4",
"markdown": "",
"html": "",
"image": null,
"featured": false,
"page": false,
"status": "draft",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2015-04-23T02:24:42.553Z",
"created_by": 1,
"updated_at": "2015-04-23T02:24:42.553Z",
"updated_by": 1,
"published_at": null,
"published_by": null,
"tags": [],
"author": 1,
"url": "/untitled-4/"
}]
}
Ideas for core editing paradigm:
Other enhancements:
The cover image could be displayed as the background of the header.
I need to make sure users are able to access older posts too. Ghost's API limits each response to 15 posts.
Right now we query the server in the onCreate
of PostListActivity
, which is obviously very expensive. A better solution would be to fetch data once from the server and cache it in a database. There are several things to consider:
PostListActivity#onResume()
, perform updatePostListActivity#onPause()
, cancel all scheduled updatesNotes about implementation:
SyncAdapter
isn't needed here, as we won't be syncing anything periodically, pushing notifications, etc.SyncAdapter
out of the picture, a ContentProvider
also becomes optional. The advantage with a ContentProvider
is that we get notifications on db updates. But many db libraries support that:
Overall, Realm seems like the best alternative at this point. Advantages over others:
ContentProvider
/ Loader
/ Cursor
APIsDisadvantages:
Waiting for AndDown v0.2.3 to be published.
How-To: https://github.com/evant/gradle-retrolambda :)
java.lang.RuntimeException: Unable to start activity ComponentInfo{me.vickychijwani.spectre/me.vickychijwani.spectre.view.PostListActivity}: java.lang.RuntimeException: Could not dispatch event: class me.vickychijwani.spectre.event.LoadUserEvent to handler [EventHandler public void me.vickychijwani.spectre.network.NetworkService.onLoadUserEvent(me.vickychijwani.spectre.event.LoadUserEvent)]: Attempt to invoke interface method 'void me.vickychijwani.spectre.network.GhostApiService.getCurrentUser(retrofit.Callback)' on a null object reference
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2298)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360)
at android.app.ActivityThread.access$800(ActivityThread.java:144)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Method.java)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Caused by: java.lang.RuntimeException: Could not dispatch event: class me.vickychijwani.spectre.event.LoadUserEvent to handler [EventHandler public void me.vickychijwani.spectre.network.NetworkService.onLoadUserEvent(me.vickychijwani.spectre.event.LoadUserEvent)]: Attempt to invoke interface method 'void me.vickychijwani.spectre.network.GhostApiService.getCurrentUser(retrofit.Callback)' on a null object reference
at com.squareup.otto.Bus.throwRuntimeException(Bus.java:458)
at com.squareup.otto.Bus.dispatch(Bus.java:388)
at com.squareup.otto.Bus.dispatchQueuedEvents(Bus.java:369)
at com.squareup.otto.Bus.post(Bus.java:338)
at me.vickychijwani.spectre.view.PostListActivity.onCreate(PostListActivity.java:82)
at android.app.Activity.performCreate(Activity.java:5933)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1105)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2251)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360)
at android.app.ActivityThread.access$800(ActivityThread.java:144)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Method.java)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void me.vickychijwani.spectre.network.GhostApiService.getCurrentUser(retrofit.Callback)' on a null object reference
at me.vickychijwani.spectre.network.NetworkService.onLoadUserEvent(NetworkService.java:99)
at java.lang.reflect.Method.invoke(Method.java)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.squareup.otto.EventHandler.handleEvent(EventHandler.java:89)
at com.squareup.otto.Bus.dispatch(Bus.java:386)
at com.squareup.otto.Bus.dispatchQueuedEvents(Bus.java:369)
at com.squareup.otto.Bus.post(Bus.java:338)
at me.vickychijwani.spectre.view.PostListActivity.onCreate(PostListActivity.java:82)
at android.app.Activity.performCreate(Activity.java:5933)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1105)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2251)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360)
at android.app.ActivityThread.access$800(ActivityThread.java:144)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Method.java)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Crashlytics is probably the simplest and best way to log all the random crashes automatically. I should do this asap, to ensure all crashes are logged and fixed through normal usage.
There's a lot more information about this crash on crashlytics.com:
https://crashlytics.com/individual16/android/apps/me.vickychijwani.spectre/issues/54ef4e4f7d7854d7c9909ec5
I could do English <-> Traditional Chinese (Hong Kong) translation, but dunno where to start off, any idea?
This issue belongs to the create / edit post epic: #5.
Again, Ghost's PUT /posts
call is very forgiving :)
Libraries for implementing UI:
Full HTTP request / response:
PUT /posts/<id>
Content-Type: application/json; charset=utf-8
Authorization: Bearer <access_token>
{
"posts": [{
"title": "<title>",
"markdown": "<markdown>",
"status": "draft",
"tags": [{
"name": "<tag1>"
}, {
"name": "<tag2>"
}]
}]
}
200 OK
{
"posts": [{
"id": 27,
"uuid": "5f04e432-67e4-48be-a3b8-0a3beb3d5b0c",
"title": "<title>",
"slug": "<slug>",
"markdown": "<markdown>",
"html": "<html>",
"image": null,
"featured": false,
"page": false,
"status": "draft",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"created_at": "2015-04-23T02:24:42.553Z",
"created_by": 1,
"updated_at": "2015-04-23T02:24:42.553Z",
"updated_by": 1,
"published_at": null,
"published_by": null,
"tags": [{
"id": 12,
"uuid": "fd9f1a43-ef3c-4b96-ba22-17b6f99e29c3",
"name": "<tag1>",
"slug": "<slug>",
"description": null,
"image": null,
"hidden": false,
"meta_title": null,
"meta_description": null,
"created_at": "2015-05-09T19:44:38.942Z",
"created_by": 1,
"updated_at": "2015-05-09T19:44:38.942Z",
"updated_by": 1,
"parent": null
}, {
"id": 13,
"uuid": "a3724452-9932-4896-b3b7-7c92f4373cff",
"name": "<tag2>",
"slug": "<slug>",
"description": null,
"image": null,
"hidden": false,
"meta_title": null,
"meta_description": null,
"created_at": "2015-05-09T19:45:15.008Z",
"created_by": 1,
"updated_at": "2015-05-09T19:45:15.008Z",
"updated_by": 1,
"parent": null
}],
"author": 1,
"url": "<slug>"
}]
}
Why you not committing? ๐ ๐
The reason is that when the post is created, it has a status of LOCAL_NEW
, and then the status changes to PUBLISHED
, so it cannot be created on the server when a network connection is available later.
The post state machine will need to be updated to accommodate this case. Current thoughts are to have 2 orthogonal fields:
status
: must be DRAFT
or PUBLISHED
onlypendingActions
: can be one or more of: CREATE
, EDIT
(there are only 2 actions now, but more may be added later, like DELETE
)Steps to reproduce:
This behaviour is annoying as it breaks the flow when writing a post: write something, preview, go back and make edits, preview, ...
This issue belongs to the create / edit post epic: #5.
This might be an issue on Realm's end. I've opened realm/realm-java#1068 for it.
EDIT: this is a bug in Quill. Explanation here.
There are 3 cases:
I think the 3rd case is the most useful one. Ghost will soon add has added support for previewing drafts, so it should be possible to use the API directly.
Design: http://www.google.com/design/spec/components/snackbars-toasts.html
Code: https://github.com/nispok/snackbar or https://github.com/MrEngineer13/SnackBar use the official design support library -- waiting on #73 for that.
Published posts should not have a delete option, for security reasons. They can still be deleted though, by unpublishing first.
This is an overarching "epic" issue for the create / edit post workflow. The checklist links to individual issues.
As a user, I want to be able to:
GET /ghost/api/v0.1/slugs/post/My%20awesome%20post%20title/
Authorization: Bearer <access_token>
200 OK
Cache-Control: no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0
Content-Type: application/json; charset=utf-8
ETag: W/"20-95717991"
{
"slugs": [{
"slug": "my-awesome-post-title"
}]
}
POST /ghost/api/v0.1/posts/?include=tags
Authorization: Bearer <access_token>
Content-Type: application/json; charset=UTF-8
{
"posts":[
{
"title":"Blah blah",
"slug":"blah-blah",
"markdown":"",
"image":null,
"featured":false,
"page":false,
"status":"draft",
"language":"en_US",
"meta_title":null,
"meta_description":null,
"author":"1",
"published_by":null,
"tags":[
{
"uuid":null,
"name":"c++",
"slug":null,
"description":null,
"meta_title":null,
"meta_description":null,
"image":null,
"hidden":false,
"post_count":null
},
{
"uuid":null,
"name":"another-tag",
"slug":null,
"description":null,
"meta_title":null,
"meta_description":null,
"image":null,
"hidden":false,
"post_count":null
}
]
}
]
}
201 Created
Cache-Control: no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0
Location: /ghost/api/v0.1/posts/2/?status=draft
Content-Type: application/json; charset=utf-8
{
"posts":[
{
"id":2,
"uuid":"2bf55004-6c3b-4d8b-9929-31bf46c1b620",
"title":"Blah blah",
"slug":"blah-blah",
"markdown":"",
"html":"",
"image":null,
"featured":false,
"page":false,
"status":"draft",
"language":"en_US",
"meta_title":null,
"meta_description":null,
"created_at":"2015-02-18T10:36:36.647Z",
"created_by":1,
"updated_at":"2015-02-18T10:36:36.647Z",
"updated_by":1,
"published_at":null,
"published_by":null,
"tags":[
{
"id":2,
"uuid":"6105741a-d031-4064-99b1-ef4e9a2953c6",
"name":"c++",
"slug":"c",
"description":null,
"image":null,
"hidden":false,
"meta_title":null,
"meta_description":null,
"created_at":"2015-02-18T10:36:36.695Z",
"created_by":1,
"updated_at":"2015-02-18T10:36:36.695Z",
"updated_by":1,
"parent":null
},
{
"id":3,
"uuid":"1ab0ec4a-567a-48ab-824c-6665894e83ab",
"name":"another-tag",
"slug":"another-tag",
"description":null,
"image":null,
"hidden":false,
"meta_title":null,
"meta_description":null,
"created_at":"2015-02-18T10:36:36.758Z",
"created_by":1,
"updated_at":"2015-02-18T10:36:36.758Z",
"updated_by":1,
"parent":null
}
],
"author":1,
"url":"/blah-blah/"
}
]
}
Make a PUT request to /ghost/api/v0.1/posts/2/?include=tags
with the post payload and post.status
set to published
.
PUT /ghost/api/v0.1/posts/<post_id>/?include=tags
Authorization: Bearer <access_token>
Content-Type: application/json; charset=UTF-8
{
"posts": [{
"title": "Post title",
"slug": "post-slug",
"markdown": "Here's some _Markdown_ **text**!",
"image": "",
"featured": false,
"page": false,
"status": "published",
"language": "en_US",
"meta_title": null,
"meta_description": null,
"updated_at": "2015-01-12T19:32:44.182Z",
"published_at": "2014-11-06T20:28:00.364Z",
"author": "1",
"published_by": "1",
"tags": [{
"id": "8",
"uuid": "025a5d49-b6f3-4941-b25c-676c3320684e",
"name": "c++",
"slug": "c-2",
"description": null,
"parent_id": null,
"meta_title": null,
"meta_description": null,
"image": null
}]
}]
}
200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0
X-Cache-Invalidate: /, /page/*, /rss/, /rss/*, /tag/*, /author/*, /sitemap-*.xml, /post-slug/
<same as request body, except one additional field: html generated from markdown>
Checklist:
This issue belongs to the create / edit post epic: #5.
This issue is about getting a bare-bones post editor up and running in the app. The current plan is to start with a simple text field, no UI controls, and no HTML preview. User types in Markdown, taps Save
, Markdown is sent to server which converts it to HTML.
Cases:
Steps to reproduce:
Display a toast saying "No Internet connection" or something.
(Code not committed yet).
There are several possible solutions:
I like the first one best. Note: we MUST NEVER CHANGE the slug accidentally after the post is published.
This issue belongs to the create / edit post epic: #5.
If possible, we should use the exact same Markdown implementation that Ghost uses. Ghost uses Showdown.js, which we might want to use, but it's likely to be quite slow on large blog posts. I'll try the options below first.
There are a couple of possibilities for a native Markdown implementation here:
TextView
with Spannable
s.Spannable
s directly, which means it'll be even faster and easier on the eyes than anddown's output. Cons: gfm support is buggy.At this point, anddown bypass seems like the best option overall.
Right now the app performs a full update in PostListActivity#onResume()
and every 10 minutes thereafter (until onPause()
is called). On my blog with only 11 full-length posts and 4 nearly-empty drafts, this takes up ~40 KB. For a regular blogger with 100s of posts this could easily add up to a megabyte or more of wasted bandwidth (not to mention the added battery usage).
Storing the E-Tag received in the response and then sending it in subsequent requests will alleviate this to some extent.
This issue belongs to the login epic: #1.
Upon logout:
I'm not sure if this is absolutely needed, but I'll make a note of it in any case.
Current behaviour:
User launches app => logs in (on every launch) => writes a blog post, etc => taps Back / Up which eventually takes him back to the login screen. Even though the user's credentials are remembered, the login action is not initiated automatically on app launch, which is stupid.
Expected behaviour:
User launches app => logs in (one-time only) => writes blog post, etc => taps Back / Up which takes him to the app launcher, bypassing the login screen entirely.
This will require a couple of changes:
PostListActivity
the default launcher activity].PostListActivity
check if the user is logged in, and if not, display the login screen to ask for the user's credentials.This issue belongs to the create / edit post epic: #5.
This issue belongs to the login epic: #1.
We need to handle common error scenarios on the login screen of the app:
http://incorrect.url.com
, http/blog.example.com
, file:///home/user
)example.com
, @example.com
)[email protected]
)incorrect_password
)This issue belongs to the create / edit post epic: #5.
This option should behave differently based on whether image storage is enabled in Ghost.
To upload an image:
POST /ghost/api/v0.1/uploads
Authorization: Bearer <access_token>
Content-Type: multipart/form-data; boundary=---------------------------17198750393646391371265188423
-----------------------------17198750393646391371265188423
Content-Disposition: form-data; name="uploadimage"; filename="<filename>.jpg"
Content-Type: image/jpeg
<image_data>
-----------------------------17198750393646391371265188423--
200 OK
Content-Type: application/json; charset=utf-8
/content/images/2015/08/<filename>.jpg
To check whether file storage is enabled:
GET /ghost/api/v0.1/configuration/fileStorage/
Authorization: Bearer <access_token>
200 OK
Cache-Control: no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0
Content-Type: application/json; charset=utf-8
ETag: W/"37-5d4948bf"
{
"configuration": [
{
"key": "fileStorage",
"value": false
}
]
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.