Giter VIP home page Giter VIP logo

quill's People

Contributors

comradekingu avatar cristears avatar dexafree avatar eddiezane avatar fastbyte01 avatar gitter-badger avatar guillaumevidal avatar naofum avatar sayem314 avatar sweetaaaaaa avatar vickychijwani avatar weblate avatar yffengdong avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

quill's Issues

Focus moves from post content to tags when switching to preview mode and back

Steps to reproduce:

  1. Open a post for editing
  2. Tap the content text field to show the keyboard
  3. Switch to preview mode and back
  4. Notice that the focus has moved to the tags field

This behaviour is annoying as it breaks the flow when writing a post: write something, preview, go back and make edits, preview, ...

Markdown preview in post editor

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:

  1. anddown: Pros: fast (NDK), has support for fenced code blocks, pre-built libs available. Cons: generates raw HTML, which makes it harder to use a native TextView with Spannables.
  2. bypass: Pros: fast (NDK), partial fenced code block support, used and co-maintained for Android by Trello, produces native Spannables directly, which means it'll be even faster and easier on the eyes than anddown's output. Cons: gfm support is buggy.
  3. pegdown: Pros: supports fenced code blocks (among many other extensions). Cons: apparently does not work on Android and / or is slow.

At this point, anddown bypass seems like the best option overall.

Cache data

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:

  • Initial data fetch on login
    This is pretty straightforward. Basically if we don't have the data stored locally, we download everything, populate the db, and then display the UI. Relevant tables: posts, tags, posts_tags, settings, users.
  • Data updates
    Could use E-Tags for this, Ghost's API has support. Update policy:
    • In PostListActivity#onResume(), perform update
    • If pull-to-refresh was used, perform update
    • At the end of every update, schedule the next update 600 seconds in the future
    • In PostListActivity#onPause(), cancel all scheduled updates
  • Transitional states (opened #38 to track this)
    When the user saves a draft (for example), the local representation of the post should enter a transitional state while the network request is performed in the background, during which time the UI says "Saving draft..." in the place of the draft / published status. Once the network call returns, the UI should be updated accordingly (on success: "Draft", on failure: "Draft (saved locally; error uploading to server)").

Notes about implementation:

Overall, Realm seems like the best alternative at this point. Advantages over others:

  • Dead-simple to use compared to the shitty ContentProvider / Loader / Cursor APIs
  • Offers support for notifications, although not sure how extensive that support is
  • Screaming fast, but that doesn't really matter to us

Disadvantages:

  • Realm is still in beta, so the usual caveats apply
  • Realm takes a completely different approach from Android's built-in classes, so we might run into issues where there's no equivalent available for a built-in class, or where a built-in class must be used. That doesn't seem likely, but the possibility exists.

Allow creating new drafts and saving them

This issue belongs to the create / edit post epic: #5.


Notes on implementation

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/"
    }]
}

Test cases

  • [Online] New draft => back => new post should be at the top and have the status "Draft"
  • [Offline] New draft => back => new post should be at the top and have the status "Saved locally"
  • [Offline] New draft => back => another new post should be at the top and have the status "Saved locally"
  • [Offline] Open any previously-created offline draft => back => nothing should change
  • [Offline] Open any previously-created offline draft => save => back => nothing should change
  • [Online] Swipe to refresh => status of all local drafts should change to "Draft"

Allow adding / removing tags in post editor

This issue belongs to the create / edit post epic: #5.


Again, Ghost's PUT /posts call is very forgiving :)

  • Already existing tags aren't re-created, and new ones are automatically created
  • Tags can have spaces in the name

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>"
    }]
}

Don't display login screen on Back / Up navigation

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:

  • [Make PostListActivity the default launcher activity].
  • Have PostListActivity check if the user is logged in, and if not, display the login screen to ask for the user's credentials.
  • Upon login, remember the credentials and don't ask for them again until the user logs out explicitly.
  • If the user is logged in, directly fetch and display post data.

Use E-Tags for GET /posts call

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.

Login workflow

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:

Create / edit post workflow

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:

  • Create drafts and save them
  • Edit drafts and save them
  • Publish drafts
  • Edit published posts
  • Unpublish posts

Typical requests

Post slug creation

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"
  }]
}

Draft creation

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/"
    }
  ]
}

Publish draft

Make a PUT request to /ghost/api/v0.1/posts/2/?include=tags with the post payload and post.status set to published.

Update published post

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:

Login error handling

This issue belongs to the login epic: #1.


We need to handle common error scenarios on the login screen of the app:

  • Empty fields
  • Invalid / wrong blog URL (e.g., http://incorrect.url.com, http/blog.example.com, file:///home/user)
  • Invalid email address (e.g., example.com, @example.com)
  • Unknown email address (e.g., [email protected])
  • Wrong password (e.g., incorrect_password)

Log crashes automatically using Crashlytics

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.

New posts are always published with default slug "untitled-xyz"

There are several possible solutions:

  1. When publishing, generate a slug from the title if the slug is "untitled-xyz": Even if the user publishes the post in offline mode, and if there's a slug conflict, Ghost takes care of assigning a unique slug all by itself! Tested with v0.6.2. This also respects the user's choice of slug, if it was changed on the website earlier, for example. Overall this solution seems simple and effective.
  2. Emulate the web client: Generate the slug every time the title is edited, but only for the first editing session (i.e., when the draft is created) (don't count sessions when the post didn't have a valid title). This one works offline too, but is too complex to implement.
  3. Generate and use a slug when uploading to the server: No issues if offline. The problem is, have to be careful not to change the slug for an existing post!

I like the first one best. Note: we MUST NEVER CHANGE the slug accidentally after the post is published.

Crash: NullPointerException in NetworkService.java line 99

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 issue

Help please

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!

Allow logout

This issue belongs to the login epic: #1.


Upon logout:

  • Clear the "user is logged in" flag
  • Clear cached auth token
  • Clear ALL persisted blog data (else there might be primary key conflicts later!)

Intelligent save policy when exiting post editor

Cases:

  • on back button tap, if there are any changes, then:
    • if status is "draft", save
    • if status is "published", prompt with "publish changes" / "discard" (related to #78 and #85; perhaps a common fix will work) -- this could be annoying, just auto-save
  • on home button tap / recents button tap / when a call comes in, if there are any changes, then:
    • if status is "draft", save it
    • if status is "published", save locally without uploading changes; the user must then invoke the "publish changes" option explicitly
  • provide an overflow menu option to discard changes

Image uploads in post editor

This issue belongs to the create / edit post epic: #5.


This option should behave differently based on whether image storage is enabled in Ghost.

  • Allow inserting an image by uploading it from SD card, gallery, Google Photos, Dropbox, etc
  • Allow inserting an image by URL
  • Disallow image uploads if file storage on server is disabled

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
        }
    ]
}

Post list pagination

I need to make sure users are able to access older posts too. Ghost's API limits each response to 15 posts.

Primitive post editor (simple text field, no UI controls, no HTML preview)

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.

Creating and publishing a post offline doesn't work

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 only
  • pendingActions: can be one or more of: CREATE, EDIT (there are only 2 actions now, but more may be added later, like DELETE)

Allow deleting drafts

Published posts should not have a delete option, for security reasons. They can still be deleted though, by unpublishing first.

Make delightful post editor UX

Ideas for core editing paradigm:

  1. WYSIWYG
  2. Block-based (WYSIWYG or Markdown)
  3. http://blog.codinghorror.com/what-you-cant-see-you-cant-get/
  4. WYSIWYM (semantic editing)
  5. Two editing modes: writing mode and styling mode
    • Writing workflow looks like this: first write your blog post in plain text (sans Markdown etc) in writing mode, then switch to styling mode and quickly format and style the content (in styling mode, words can be selected simply by tapping)

Other enhancements:

  • Make text selection snap to word boundaries, that should make selection 10x less frustrating
  • Syntax highlight raw Markdown in edit mode (aka "inline Markdown preview")
  • Syntax highlight code in preview mode

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.