Giter Club home page Giter Club logo

boostnote-mobile's Introduction

TachiJS

Build Status codecov NPM download Supported by BoostIO

Tachi(ε€ͺεˆ€) https://en.wikipedia.org/wiki/Tachi

Highly testable dead simple web server written in Typescript

  • 🏁 Highly testable. (all props in req and res are injectable so you don't have to mock at all.)
  • πŸ”§ Highly customizable.
  • πŸ’‰ Simple dependency injection.
  • ⚑ async/await request handler. (like Koa without any configurations.)
  • 🏭 Based on expressjs. (You can benefit from using this mature library)
  • βœ… Built-in request body validator.
  • πŸ“ Written in Typescript.

Why?

Nest.js looks nice. But its learning curve is too stiff.(TBH, I still don't know how to redirect dynamically.) Most of people probably do not need to know how Interceptor, Pipe and other things work. It might be good for some enterprize level projects.

But using raw expressjs is also quite painful. To test express apps, you have to use supertest or chai-http things. If you use them, you will lose debugging and error stack while testing because they send actual http request internally. Otherwise, you have to mock up all params, req, res and next, of RequestHandler of express.js.

To deal with the testing problem, inversify-express-utils could be a solution. But it does not support many decorators. To render with view engine like pug, we need to use res.render method. But the only solution is using @response decorator. It means you have to mock up Response in your test. So technically it is super hard to test routes rendering view engine.

Luckily, TachiJS tackles those problems. If you have other ideas, please create an issue!!

How to use

Install tachijs

npm i tachijs reflect-metadata

Add two compiler options, experimentalDecorators and emitDecoratorMetadata, to tsconfig.json.

{
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    ...
  }
}

Quick start

import tachijs, { controller, httpGet } from 'tachijs'

@controller('/')
class HomeController() {
  // Define when this method should be used.
  @httpGet('/')
  index() {
    return {
      message: 'Hello, world!'
    }
  }
}

// Register `HomeController`
const app = tachijs({
  controllers: [HomeController]
})

// `app` is just an express application instance
app.listen(8000)

Now you can access http://localhost:8000/.

For other http methods, tachijs provides @httpPost, @httpPut, @httpPatch, @httpDelete, @httpOptions, @httpHead and @httpAll.

Configuring express app(Middlewares)

There are lots of ways to implement express middlewares.

Use before and after options

import bodyParser from 'body-parser'
import { ConfigSetter, NotFoundException } from 'tachijs'

const before: ConfigSetter = app => {
  app.use(bodyParser())
}

const after: ConfigSetter = app => {
  app.use('*', (req, res, next) => {
    next(new NotFoundException('Page does not exist.'))
  })

  const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
    const { status = 500, message } = error
    res.status(status).json({
      status,
      message
    })
  }
  app.use(errorHandler)
}

const app = tachijs({
  before,
  after
})

app.listen(8000)

Without before or after options

Identically same to the above example.

import express from 'express'
import bodyParser from 'body-parser'
import { ConfigSetter, NotFoundException } from 'tachijs'

const app = express()
app.use(bodyParser())

tachijs({
  app
})

app.use('*', (req, res, next) => {
  next(new NotFoundException('Page does not exist.'))
})

const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
  const { status = 500, message } = error
  res.status(status).json({
    status,
    message
  })
}
app.use(errorHandler)

app.listen(8000)

Apply middlewares to controllers and methods

Sometimes, you might want to apply middlewares to several methods only.

import { controller, httpGet, ForbiddenException } from 'tachijs'
import cors from 'cors'
import { RequestHandler } from 'express'

const onlyAdmin: RequestHandler = (req, res, next) => {
  if (!req.user.admin) {
    next(new ForbiddenException('Only admin users can access this api'))
    return
  }
  next()
}

// Apply `cors()` to controller. Now all methods will use the middleware.
@controller('/', [cors()])
class HomeController() {
  @httpGet('/')
  index() {
    return {
      message: 'Hello, world!'
    }
  }

  // Apply `onlyAdmin` to `admin` method. This middleware will be applied to this method only.
  @httpGet('/', [onlyAdmin])
  admin() {
    return {
      message: 'Hello, world!'
    }
  }
}

Configure router options

Tachijs will create and register a router for each controller.

So you can provide router options via @controller decorator.

@controller('/:name', [], {
  // Provide mergeParams option to express router.
  mergeParams: true
})
class HomeController {
  @httpGet('/hello')
  // Now routes in the controller can access params.
  index(@reqParams('name') name: string) {
    return `Hello, ${name}`
  }
}

Access req.params, req.query and req.body via decorators

You can access them via @reqParams, @reqQuery and @reqBody. (Don't forget to apply body-parser middleware)

import {
  controller,
  httpGet,
  httpPost,
  reqParams,
  reqQuery,
  reqBody
} from 'tachijs'

@controller('/posts')
class PostController() {
  @httpGet('/:postId')
  // `req.params.postId`
  async show(@reqParams('postId') postId: string) {
    const post = await Post.findById(postId)

    return {
      post
    }
  }

  @httpGet('/search')
  // `req.query.title`
  async search(@reqQuery('title') title: string = '') {
    const posts = await Post.find({
      title
    })

    return {
      posts
    }
  }

  @httpPost('/')
  // `req.body` (`@reqBody` does not accept property keys.)
  async create(@reqBody() body: unknown) {
    const validatedBody = validate(body)
    const post = await Post.create({
      ...validatedBody
    })

    return {
      post
    }
  }
}

We also provide reqHeaders, reqCookies and reqSession for req.headers, req.cookies and req.session. To know more, see our api documentation below.

Body validation

@reqBody supports validation via class-validator.

Please install class-validator package first.

npm install class-validator
import { IsString } from 'class-validator'

class PostDTO {
  @IsString()
  title: string

  @IsString()
  content: string
}


@controller('/posts')
class PostController() {
  @httpPost('/')
  // Tachijs can access `PostDTO` via reflect-metadata.
  async create(@reqBody() body: PostDTO) {
    // `body` is already validated and transformed into an instance of `PostDTO`.
    // So we don't need any extra validation.
    const post = await Post.create({
      ...body
    })

    return {
      post
    }
  }
}

Custom parameter decorators!

If you're using passport, you should want to access user data from req.user. @handlerParam decorator make it possible. The decorator gets a selector which accepts express's req, res and next. So all you need to do is decide what to return from thoes three parameters.

import { controller, httpGet, handlerParam } from 'tachijs'

@controller('/')
class HomeController {
  @httpGet('/')
  async showId(@handlerParam((req, res, next) => req.user) user: any) {
    doSomethingWithUser(user)

    return {
      ...
    }
  }
}

If you want reusable code, please try like the below.

import { controller, httpGet, handlerParam } from 'tachijs'

function reqUser() {
  // You can omit other next params, `res` and `next`, if you don't need for your selector.
  return handlerParam(req => req.user)
}

@controller('/')
class HomeController {
  @httpGet('/')
  async showId(@reqUser() user: any) {
    doSomethingWithUser(user)

    return {
      ...
    }
  }
}
Bind methods of req or res before exposing

You can also pass methods of req or res which are augmented by express module. Some of them might need the context of them. So please bind methods before exposing like the below example.

export function cookieSetter() {
  return handlerParam((req, res) => res.cookie.bind(res))
}
design:paramtype

Moreover, tachijs exposes metadata of parameters to forth argument. So you can make your custom validator for query with class-transformer-validator like below. (req.body is also using this.)

import { controller, httpGet, handlerParam } from 'tachijs'
import { IsString } from 'class-validator'
import { transformAndValidate } from 'class-transformer-validator'

function validatedQuery() {
  return handlerParam((req, res, next, meta) => {
    // meta.paramType is from `design:paramtypes`.
    // It is `Object` if the param type is unknown or any.
    return meta.paramType !== Object
      ? transformAndValidate(meta.paramType, req.query)
      : req.query
  })
}

// Validator class
class SearchQuery {
  @IsString()
  title: string
}

@controller('/')
class PostController {
  @httpGet('/search')
  // Provide the validator class to param type.
  // tachijs can access it via `reflect-metadata`.
  search(@validatedQuery() query: SearchQuery) {
    // Now `query` is type-safe
    // because it has been validated and transformed into an instance of SearchQuery.
    const { title } = query

    return {
      ...
    }
  }
}

To know more, see @handlerParam api documentation below.

Redirection, Rendering via pug and others...

Techinically, you don't have to access res to response data. But, if you want to redirect or render page via pug, you need to access res.redirect or res.render. Sadly, if you do, you have make mockup for res.

But, with tachijs, you can tackle this problem.

import { controller, httpGet, RedirectResult } from 'tachijs'

@controller('/')
class HomeController {
  @httpGet('/redirect')
  redirectToHome() {
    return new RedirectResult('/')
  }
}

Now, you can test your controller like the below example.

describe('HomeController#redirectToHome', () => {
  it('redirects to `/`', async () => {
    // Given
    const controller = new HomeController()

    // When
    const result = controller.redirectToHome()

    // Then
    expect(result).toBeInstanceOf(RedirectResult)
    expect(result).toMatchObject({
      location: '/'
    })
  })
})

There are other results too, EndResult, JSONResult, RenderResult, SendFileResult, SendResult, and SendStatusResult. Please see our api documentation below.

BaseController

If you need to use many types of result, you probably want BaseController. Just import it once, and your controller can instantiate results easily.

import { controller, httpGet, BaseController } from 'tachijs'

@controller('/')
// You have to extend your controller from `BaseController`
class HomeController extends BaseController {
  @httpGet('/redirect')
  redirectToHome() {
    // This is identically same to `return new RedirectResult('/')`
    return this.redirect('/')
  }
}

BaseController has methods for all build-in results, Please see our api documentation below.

BaseController#context

You may want to share some common methods via your own base controller. But, sadly, it is not possible to use decorators to get objects from req or res and services provided by @inject.

To make it possible, we introduce context. Which expose req, res and inject method via context if your controller is extended from BaseController.

interface Context {
  req: express.Request
  res: express.Response
  inject<S>(key: string): S
}
import { BaseController, controller, httpPost } from 'tachijs'

class MyBaseController extends BaseController {
  async getUserConfig() {
    // When unit testing, `context` is not defined.
    if (this.context == null) {
      return new UserConfig()
    }

    const { req, inject } = this.context

    // Now we can get the current user from `req`
    const currentUser = req.user

    // And inject any services from the container.
    const userConfigService = inject<UserConfigService>(
      ServiceTypes.UserConfigService
    )

    return userConfigService.findByUserId(userId)
  }
}

@controller('/')
class HomeController {
  @httpGet('/settings')
  settings() {
    const userConfig = await this.getUserConfig()

    return this.render('settings', {
      userConfig
    })
  }
}

#httpContext, #inject and #injector will be deprecated from v1.0.0. Please use #context

Customize result

If you want to have customized result behavior, you can do it with BaseResult. BaseResult is an abstract class which coerce you to define how to end the route by providing execute method. (Every built-in result is extended from BaseResult.)

Let's see our implementation of RedirectResult.

import express from 'express'
import { BaseResult } from './BaseResult'

export class RedirectResult extends BaseResult {
  constructor(
    public readonly location: string,
    public readonly status?: number
  ) {
    super()
  }

  // tachijs will provide all what you need and execute this method.
  async execute(
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) {
    if (this.status != null) return res.redirect(this.status, this.location)
    return res.redirect(this.location)
  }
}

Dependency injection

To make controllers more testable, tachijs provides dependency injection.

Let's think we have some mailing service, MailerService. While developing or testing, we probably don't want our server to send real e-mail everytime.

import tachijs, {
  controller,
  httpGet,
  httpPost,
  reqBody,
  inject,
  BaseController
} from 'tachijs'

// Create enum for service types
enum ServiceTypes {
  EmailService = 'EmailService',
  NotificationService = 'NotificationService'
}

// Abstract class coerce MailerService must have `sendEmail` method.
abstract class MailerService {
  abstract sendEmail(content: string): Promise<void>
}

// Mockup service for development and testing.
class MockEmailService extends MailerService {
  async sendEmail(content: string) {
    console.log(`Not sending email.... content: ${content}`)
  }
}

class EmailService extends MailerService {
  async sendEmail(content: string) {
    console.log(`Sending email.... content: ${content}`)
  }
}

interface Container {
  [ServiceTypes.EmailService]: typeof MailerService
}

const envIsDev = process.env.NODE_ENV === 'development'

// Swapping container depends on the current environment.
const container: Container = envIsDev
  ? {
      // In development env, don't send real e-mail because we use mockup.
      [ServiceTypes.EmailService]: MockEmailService
    }
  : {
      [ServiceTypes.EmailService]: EmailService
    }

@controller('/')
class HomeController extends BaseController {
  constructor(
    // Inject MailerService. The controller will get the one registered to the current container.
    @inject(ServiceTypes.EmailService) private mailer: MailerService
  ) {
    super()
  }

  @httpGet('/')
  home() {
    return `<form action='/notify' method='post'><input type='text' name='message'><button>Notify</button></form>`
  }

  @httpPost('/email')
  async sendEmail(@reqBody() body: any) {
    await this.mailer.sendEmail(body.message)

    return this.redirect('/')
  }
}

const server = tachijs({
  controllers: [HomeController],
  // Register container
  container
})

So you can test HomeController#sendEmail like the below example.

describe('HomeController#sendEmail', () => {
  it('sends email', async () => {
    // Given
    const spyFn = jest.fn()
    class TestEmailService extends MailerService {
      async sendEmail(content: string): Promise<void> {
        spyFn(content)
      }
    }
    const controller = new HomeController(new TestEmailService())

    // When
    const result = controller.sendEmail('hello')

    // Then
    expect(spyFn).toBeCalledWith('hello')
  })
})

Now we don't have to worry that our controller sending e-mail for each testing.

Furthermore, you can inject other services to your service as long as they exist in the container.

class NotificationService {
  constructor(
    // When NotificationService is instantiated, MailerService will be instantiated also by tachijs.
    @inject(ServiceTypes.EmailService) private mailer: MailerService
  ) {}

  async notifyWelcome() {
    await this.mailer.sendEmail('Welcome!')
  }
}
DI without tachijs

When some testing or just writing scripts using services, you might want to use DI without tachijs function. So we exposed Injector class which is used by tachijs.

enum ServiceTypes {
  NameService = 'NameService',
  MyService = 'MyService'
}
class NameService {
  getName() {
    return 'Test'
  }
}
class MyService {
  constructor(
    @inject(ServiceTypes.NameService) private nameService: NameService
  ) {}

  sayHello() {
    return `Hello, ${this.nameService.getName()}`
  }
}
const container = {
  [ServiceTypes.NameService]: NameService,
  [ServiceTypes.MyService]: MyService
}

// Create injector
const injector = new Injector(container)

// Instantiate by a key
const myService = injector.inject<MyService>(ServiceTypes.MyService)
// Instantiate by a constructor
const myService = injector.instantiate(MyService)

Bad practices

Please check this section too to keep your controllers testable.

Execute res.send or next inside of controllers or @handlerParam

Please don't do that. It just make your controller untestable. If you want some special behaviors after your methods are executed, please try to implement them with BaseResult.

Do

class HelloResult extends BaseResult {
  async execute(
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) {
    res.send('Hello')
  }
}

class HomePageController extends BaseController {
  @httpGet('/')
  index() {
    // Now we can test it by just checking the method returns an instance of `HelloResult`.
    return new HelloResult()
  }
}

Don't

class HomePageController {
  @httpGet('/')
  index(@handlerParam((req, res) => res) res: expressResponse) {
    // We have to make mock-up for express.Response to test
    res.send('Hello')
  }
}

Access BaseController#context in your descendant controllers

It is designed to be used inside of your base controller to make unit testing easy.

Do

class MyBaseController extends BaseController {
  doSomethingWithContext() {
    if (this.context == null) {
      // on unit testing
      return
    }
    // on live
  }
}

Don't

class HomePageController extends MyBaseController {
  @httpGet('/')
  index() {
    // We have to make mock-up everything to test
    this.context!.req....
  }
}

APIs

tachijs(options: TachiJSOptions): express.Application

Create and configure an express app.

TachiJSOptions

interface TachiJSOptions<C = {}> {
  app?: express.Application
  before?: ConfigSetter
  after?: ConfigSetter
  controllers?: any[]
  container?: C
}

type ConfigSetter = (app: express.Application) => void
  • app Optional. If you provide this option, tachijs will use it rather than creating new one.
  • before Optional. You can configure express app before registering controllers for applying middlewares.
  • after Optional. You can configure express app before registering controllers for error handling.
  • controllers Optional. Array of controller classes.
  • container Optional. A place for registered services. If you want to use DI, you have to register services to here first.

@controller(path: string, middlewares: RequestHandler[] = [], routerOptions: RouterOptions = {})

It marks class as a controller.

  • path Target path.
  • middlewares Optional. Array of middlewares.
  • routerOptions Optional. Express router options.

@httpMethod(method: string, path: string, middlewares: RequestHandler[] = [])

It marks method as a request handler.

  • method Target http methods, 'get', 'post', 'put', 'patch', 'delete', 'options', 'head' or 'all' are available. ('all' means any methods.)
  • path Target path.
  • middlewares Optional. Array of middlewares.

tachijs also provides shortcuts for @httpMethod.

  • @httpGet(path: string, middlewares: RequestHandler[] = [])
  • @httpPost(path: string, middlewares: RequestHandler[] = [])
  • @httpPut(path: string, middlewares: RequestHandler[] = [])
  • @httpPatch(path: string, middlewares: RequestHandler[] = [])
  • @httpDelete(path: string, middlewares: RequestHandler[] = [])
  • @httpOptions(path: string, middlewares: RequestHandler[] = [])
  • @httpHead(path: string, middlewares: RequestHandler[] = [])
  • @httpAll(path: string, middlewares: RequestHandler[] = [])

@handlerParam<T>(selector: HandlerParamSelector<T>)

  • selector selects a property from req, res, next or even our meta
export type HandlerParamSelector<T> = (
  req: express.Request,
  res: express.Response,
  next: express.NextFunction,
  meta: HandlerParamMeta<T>
) => T
interface HandlerParamMeta<T> {
  index: number
  selector: HandlerParamSelector<T>
  paramType: any
}
  • index Number index of the parameter.
  • selector Its selector.
  • paramType metadata from design:paramtypes.

@reqBody(validator?: any)

Inject req.body.

  • validator Optional. A class with decorators of class-validator. tachijs will validate req.body with it and transform req.body into the validator class. If validator is not given but the parameter has a class validator as its param type, tachijs will use it via reflect-metadata.
import { controller, httpPost, reqBody } from 'tachijs'

@controller('/post')
class PostController {
  @httpPost('/')
  // Identically same to `create(@reqBody(PostDTO) post: PostDTO)`
  create(@reqBody() post: PostDTO) {
    ...
  }
}

@reqParams(paramName?: string)

Inject req.params or its property.

  • paramName If it is given, req.params[paramName] will be injected.

@reqQuery(paramName?: string)

Inject req.query or its property.

  • paramName If it is given, req.query[paramName] will be injected.

@reqHeaders(paramName?: string)

Inject req.headers or its property.

  • paramName If it is given, req.headers[paramName] will be injected.

@reqCookies(paramName?: string)

Inject req.cookies or its property.

  • paramName If it is given, req.cookies[paramName] will be injected.

@reqSignedCookies(paramName?: string)

Inject req.signedCookies or its property.

  • paramName If it is given, req.signedCookies[paramName] will be injected.

@cookieSetter()

Inject res.cookie method to set cookie.

@cookieClearer()

Inject res.clearCookie method to clear cookie.

@reqSession(paramName?: string)

Inject req.session.

BaseController

A base for controller which have lots of helper methods for returning built-in results. Also, it allows another way to access properties of req, res and inject without any decorators.

  • #context tachijs will set req, res and inject method to this property. So, when unit testing, it is not defined.
    • #context.req Raw express request instance
    • #context.req Raw express response instance
    • #inject<S>(key: string): S A method to access a registered service by the given key. It is almost same to @inject decorator. (@inject<ServiceTypes.SomeService> someService: SomeService => const someService = this.inject<SomeService>(ServiceTypes.SomeService))
  • #end(data: any, encoding?: string, status?: number): EndResult
  • #json(data: any, status?: number): JSONResult
  • #redirect(location: string, status?: number): RedirectResult
  • #render(view: string, locals?: any, callback?: RenderResultCallback, status?: number): RenderResult
  • #sendFile(filePath: string, options?: any, callback?: SendFileResultCallback, status?: number): SendFileResult
  • #send(data: any, status?: number): SendResult
  • #sendStatus(status: number): SendStatusResult

Results

BaseResult

All of result classes must be extended from BaseResult because tachijs can recognize results by instanceof BaseResult.

It has only one abstract method which must be defined by descendant classes.

  • execute(req: express.Request, res: express.Response, next: express.NextFunction): Promise<any> tachijs will use this method to finalize response.

new EndResult(data: any, encoding?: string, status: number = 200)

tachijs will finalize response with res.status(status).end(data, encoding).

new JSONResult(data: any, status: number = 200)

tachijs will finalize response with res.status(status).json(data).

new NextResult(error?: any)

tachijs will finalize response with next(error).

new RedirectResult(location: string, status?: number)

tachijs will finalize response with res.redirect(location) (or res.redirect(status, location) if the status is given).

new RenderResult(view: string, locals?: any, callback?: RenderResultCallback, status: number = 200)

tachijs will finalize response with res.status(status).render(view, locals, (error, html) => callback(error, html, req, res, next))

type RenderResultCallback = (
  error: Error | null,
  html: string | null,
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) => void

new SendFileResult(filePath: string, options: any, callback?: SendFileResultCallback, status: number = 200)

tachijs will finalize response with res.status(status).sendFile(filePath, options, (error) => callback(error, req, res, next))

type SendFileResultCallback = (
  error: Error | null,
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) => void

new SendResult(data: any, status: number = 200)

tachijs will finalize response with res.status(status).send(data).

new SendStatusResult(status: number)

tachijs will finalize response with res.sendStatus(status).

@inject(key: string)

Inject a registered service in container by the given key.

class Injector

new Injector<C>(container: C)

Instantiate an injector with container

#instantiate(Constructor: any): any

Instantiate a service constructor. If the constructor has injected services, this method instantiate and inject them by #inject method.

#inject<S = any>(key: string): S

Instantiate a service by a key from Container. If there is no service for the given key, it will throws an error.

License

MIT Β© Junyoung Choi

boostnote-mobile's People

Contributors

asmsuechan avatar kazup01 avatar kohei-takata avatar rmevans9 avatar robbawebba avatar rokt33r avatar shogochiai avatar sosukesuzuki avatar vicv 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  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

boostnote-mobile's Issues

Sync desktop notes with mobile (not the other way around)

It appears that we can now sync special mobile notes with the Desktop.

However, I think most users want the other way around. Most of us already have notes on the desktop (already synced with Dropbox), and we want to be able to see (an edit) these notes on mobile.

Android app is excruciatingly slow...

Hello

I came to Boostnote, from Simplenote, and from OneNote before that... πŸ˜„
What pleased me, beside the nicely written articles on Medium πŸ˜‰ was:

  • Sync via Dropbox (among others). That's nice. Have to check how concurrent updates are handled though (eg. on phone out of network, at home on same note, then sync.)
  • Readable storage format (Cson) on accessible folder... Simplenote uses Json, putting a whole note on one line, unusable for editing outside of the app. And that's only if I export the notes. OneNote is even more opaque. Somehow, if you don't have an online version of Boostnote, it might be a way to workaround this.
  • Better Markdown handling.
  • Folders! Tags are nice, but still messy for me. Multilevel folders could be nice though.

Anyway, I made some notes (copy / paste / improve from Simplenote) on the desktop app, and I wanted to edit a medium-sized note (around 10,000 chars) on my phone.
I grant you, it is a low-end phone, already old when I bought it: a Wiko Darkside running Android 4.2.1...
But most apps work rather well on it. At worst, I have, sometime, very slow typing in Quora (in browser) or Instagram (in app), but not always.

So I launched Boostnote, selected Dropbox from All notes (because All notes only shows two placeholder notes "Tap here and write something"), selected my note. It opened at the end, so I scrolled up to a third of the text length, to reach the place where I have to type.
Already a problem: scrolling is veeery slow. Sometime, I have to move my finger 2 or 3 times before its scrolls...
OK, reached my point, I remove some stuff and start to type. Ouch! What I type is displayed after 1 to 5 seconds... per letter!
I wondered if it was my phone being slow. I closed some apps, I went to Firefox, then to JsFiddle where I could type a sentence at normal speed.
I copy it to paste it in BN. But when I started it again:

  • it was back to All notes; I would appreciate if it opened again the note I last edited, at the place I was.
  • Went to my note: all stuff (not much fortunately) which I typed so slowly was just no longer there...

A rather bad experience, if I can say so. Not sure if that's a problem with ReactNative, with CodeMirror, or some other layer, but for me, the app is just not usable, which reduces considerably the interest of BN, alas.

Note: preview scrolls at normal speed. But has another bug, which I will fill separately.

How to set local documents directory on Android?

I couldn't find how to change default path where Boostnote stores local documents. It seems it's hardcoded as /data/data/.... without an option to change it for any other path. I'm not using Dropbox and I use FolderSync to sync my Google Drive selected folders, but using system protected folder /data/data/... for sync would require from me to grant FolderSync su rights which I wouldn't like to do. Why does Boostnote use protected system folder instead of /sdcard/... (e.g. /sdcard/Documents or /sdcard/Boostnote or /sdcard/Android/... ? Is there any walkaround or another solution?

Title bar does not mesh well on the iPhone X

It appears that you are using a older version of native-base that does not have the fix for the iPhone X header in. This makes the usability on iPhone X pretty terrible.

I upgraded to the latest native-base (and react-native to support changes in native-base) and all seems mostly well. There are a few minor things I still need to try and work out like in the add note modal the "helper" buttons at the bottom are slightly off screen. Likely because of the differing header sizes.

Can't scroll on Dropbox screen

Screen capture: https://www.youtube.com/watch?v=xGVU8QtAR2o

When I attempt to scroll through my notes on the Dropbox screen, the "refresh" button scrolls on the screen with the notes. Also, this is somehow preventing me from seeing the rest of my notes (I have 49 notes in Dropbox, but even with the minor amount of scrolling in that video, you can see that nowhere near to 49 are being shown)

! Exclamation mark causes line break in code block

Just installed on android 7.1.2 today.

Wrote a code block:

println!("hello world");

Which rendered as (using the preview):

println
!("hello world");

Experimenting a little; it is clear that every ! is printed on a new line when previewed.

Has an exception been made to cause this behaviour?

Personally it is very unhelpful. For me, expected behaviour would be: never add line breaks in a code block: all characters should be rendered as is - simple as that.

Table not rendering on Android

Hi,

Markdown tables only show up as lines. Any solution? I've tried aligning them properly but still not working.

Thank you!

Android app crashes when you press "Dropbox" in menu

I set up Boostnote mobile to connect to Dropbox, and it seems to have got past the auth stage (I put a key in). However, the list is still empty, and whenever I press "Dropbox" in the menu, the app immediately crashes. Just behind the menu, as it crashes, you can see a bit of a "Sign in to Dropbox" button.

This happens every time, it's never got further than that.

No Scrolling in Preview Mode

It seems that within the iOS app on my iPhone 7+ there is no scrolling in the preview pane. So currently while editing a note you can simply scroll like normal. But when you enter into the preview / viewing pane you can no longer scroll with one finger. This happens within the local files as well as the drop box files. When on the physical device you can engage the scrolling with two fingers for some reason, and then within the simulator in xcode you can not get it to scroll with input.


From the simulator:

ezgif com-gif-maker

Dropbox Sync Multiple storages

Hello.
Can I have multiple storage folders synced with Dropbox? I move all my storages inside the "boostnote-mobile" and its not working so I guess the "boostnote-mobile" should be the "root" folder of a storage or am I wrong?

That makes it almost useless for me right now, since I have like 5 or 6 storage folders.

Monospace font

Currently, the font is not a monospaced one. This, unfortunately, makes the app unusable for Markdown notes and code snippets.

Design issues in preferences window

I'm using Boostnote v0.8.17 on Ubuntu 17.10 64bit, and found two issues.

  • Strange black (check)box at the top of the preferences window.
  • "Info: Release Note: v0.8.17" is too large for the sidebar.

boostnote bug ubuntu 17 10

Feature Request: Use iCloud for cloud storage

Issuehunt badges

I created my Boostnote storage for macOS using iCloud but cannot access it from the iOS version. I can see my Boostnote storage folder using the new β€œFiles” app in iOS 11.x, but cannot get the iOS Boostnote app to read them.

I think this feature would be relatively easy low-hanging fruit, and it’s the only thing keeping me from completely ditching Evernote.


IssueHunt Summary

Backers (Total: $50.00)

  • $50.00 have been anonymously funded.

Become a backer now!

Or submit a pull request to get the deposits!

Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

MD keys >> unwanted space (probably autocorrect)

HI,

Just tested the new MD keys in the mobile app.
Great improvement

But... when hitting the dash (#) key, the editor automatically adds a whitespace...
Hence I have to backspace before hitting the dash a second time...

I'm kind a guessing this is due to autocorrect...
But you guys are much smarter than I am

This is rather annoying though

dGo

Google Drive Support?

Will Google Drive get implemented or should I move to Dropbox for the storage support?

Add optional pin/fingerprint protection

Simplenote (and I'm sure - others) has this feature. Whenever the app is opened (or device is unlocked while app is open, or app is switched to from multitasking) it will require a pin (or, optionally, a fingerprint) in order to access the notes. Based on the user's preference, obviously

Cannot authorize DropBox on Boostnote Android

Screenshot: screenshot

It seems like the "Send!" button doesn't "enable" when I paste in a token. Regardless, it allows me to click the button and displays this message. Have tried this process twice now (meaning trying two different authentication tokens)

Keyboard bug

Hello,

I use Boostnote on iOS in iPH7+,

when I use keyboard I have only this bug in picture, when switched between preview and text editor,

img_5955

if you can fix this bug the can be very well

thinks

Scrolling preview closes the note

In my previous issue, I reported a very slow Android application, making typing almost impossible, but I said that the previous could be scrolled quite fast.
But I also experienced some slow downs, although not consistently.
There is one odd thing, though: I open the preview, I can scroll down (toward the end), sometime I can scroll up, but sometime (and always, when I reached the end of the note), it closes the note!
It seems like a "feature" as it does that with an animation effect of the note leaving place to the folder view...
Although it is more usually done with a side sweep, not a vertical one.

MD keys >> * for italic instead of _

Love the MD keys in the mobile app.

But, I personally prefer to use a single * for italic...
not the underscore _

Would be nice to have an option to change this behavior

dGo

iPad version?

Very excited that Boostnote is making it to mobile platforms! I wonder if an iPad version is in the works? The iOS version seems to be for iPhone, with fixed orientation.

Thanks for a great note app!

iPad: Pro support + landscape

Thanks for the iPad version, but the current app does not support landscape and it is not optimized for the iPad Pro.

Support iOS Files

If the new Files would be supported, you'd automatically have support for all major cloud storage providers,

  • Dropbox,
  • One Drive
  • Box
  • iCloud
  • Google Drive
  • Nextcloud
  • Seafile

as well as local storage.

Our organization is not allowing the usage of Dropbox, so we're out of luck syncing Boostnote to our mobile clients. Supporting Files should be quite easy.

This would also close several other issues like #94.

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.