kite-project / hope

A new web experience for your B2G and Android devices.
8 stars 3 forks source link

Consider managing page state with objects #43

Open jonathanKingston opened 8 years ago

jonathanKingston commented 8 years ago

Consider a change to manage page state with an object that knows it's own history and how the user has interacted with it. This should allow the page:

This would then be thrown away on navigation or passed to a new page object which would could have it's own timers, state etc.

Super rough example code:

const DEFAULT_THEME = '#56565A';

window.HopeTab = fxosComponent.register('hope-tab', {
  defaultContext = {
     theme: DEFAULT_THEME
  },
  created() {
    on(iframe, 'mozbrowserlocationchange', e => this.onLocationChange(e));
    on(iframe, 'mozbrowsertitlechange', e => this.onTitleChange(e));
    on(iframe, 'mozbrowsermetachange', e => this.currentPage.onMetaChange(e));
    on(iframe, 'mozbrowsererror', e => this.currentPage.onError(e));
    on(this.els.close, 'click', e => this.onCloseClick(e));
    on(this.els.form, 'submit', e => this.onSubmit(e));
    on(this.els.refresh, 'click', e => this.refresh(e));
    on(this.els.input, 'focus', e => this.onInputFocus(e));
  },
  currentPage: null,
  sanitizeUrl(url) {
    let outputURL = url;
    ...
    return new URL(outputUrl);
  },
  onSubmit: function (e) {
    e.preventDefault();
    let url = this.els.input.value;
    this.els.input.blur();
    let page = this.setPage(url, defaultContext);
    page.userSubmittedUrl = true;
  },
  onLocationChange: function (e) {
    let context = this.currentPage.context;
    this.setPage(e.detail, context);
  },
  setPage: function (url, context) {
    this.url = url;
    let urlObject = this.sanitizeUrl(url);
    this.currentPage = new Page(urlObject, Object.assign({}, context));
    return this.currentPage;
  }
});

class Page {
  constructor(urlObject, context) {
    this.userSubmittedUrl = false;
    this.url = urlObject;
    // Not sure if current state needs passing through?
    this.context = Object.assign({}, context);
    this.hasOverrideTheme();
  }
  hasOverrideTheme() {
    /* set important sites theme...
    if (isFB) {
      this.theme = site.color;
    }*/
  },
  onMetaChange() {
    var meta = e.detail;

    switch (meta.name) {
      case 'theme-color': this.theme = meta.content; break;
    }

  }
  set theme(color) {
    this.context.theme = color;
    this.showTheme();
  }
  get theme() {
    return this.context.theme;
  }
  showTheme() {
    let shownColor = this.context.theme;
    if (this.context.securityState === 'insecure') {
      shownColor = 'red';
    }
    if (this.context.securityState === 'broken') {
      shownColor = DEFAULT_THEME;
    }
    //set URL bars etc
  }
  firstPaint() {
    if (!('theme' in this.context)) {
      this.theme = DEFAULT_THEME;
    }
  },
  error(e) {
    if (this.userSubmittedUrl) {
      // Handle degrade to http
    }
    if (e.detail === 'certError') {
      // ...
      this.showTheme();
    }
  }
  securityChange(e) {
    this.context.securityState = e.details.state;
    this.showTheme();
  }
}

@etiennesegonzac and @wilsonpage thoughts?

etiennesegonzac commented 8 years ago

I like the general idea but I'm not sure I fully understand what would be the lifecycle of the 'context'. Is the plan to throw it away depending on heuristics (like the origin check we currently have)? Also interested in how this ties in to adding a progress indicator + support for the canGoBack / canGoForward exposed on the mozbrowser iframe.

jonathanKingston commented 8 years ago

I'm torn between suggesting that the context could be replaced with something like hasStaleState = true which the page could reset after the first paint and clean up the theme color for example. Either that or as you say the context could be modified on the way in by the heuristics on the outside and painted on the initial new Page() generation. The idea is to always get a new page object which knows about where it is in the painting and loading lifecycle etc which could be slightly in front of the colours of the bars etc because the info hasn't loaded yet.

The stale page objects could also be stored within a WeakSet perhaps to make a API similar to History.

Sorry this doesn't make much sense... I'll try and draw a state diagram soon.

On Thu, Feb 4, 2016 at 3:00 PM Etienne Segonzac notifications@github.com wrote:

I like the general idea but I'm not sure I fully understand what would be the lifecycle of the 'context'. Is the plan to throw it away depending on heuristics (like the origin check we currently have)? Also interested in how this ties in to adding a progress indicator + support for the canGoBack / canGoForward exposed on the mozbrowser iframe.

— Reply to this email directly or view it on GitHub https://github.com/kite-project/hope/issues/43#issuecomment-179884234.

jonathanKingston commented 8 years ago

Does canGoBack get limited by origin much like the History API would be? If not I'm not really sure handling through Objects really makes any change here other than goBack() giving us the old theme instantly.

jonathanKingston commented 8 years ago

This is perhaps a lot simpler were the heuristics are done up front to check if the interface needs to be reset, then on first paint if it still doesn't have a theme it will also default.

The page also isn't responsible for making the UX change here either.

Again this code is rough - there is GC issues etc

const DEFAULT_THEME = '#56565A';
const DEFAULT_URL = 'apps/homescreen/index.html';

window.HopeTab = fxosComponent.register('hope-tab', {
  created() {
    let url = this.getAttribute('url') || DEFAULT_URL;
    this.setPage(url);

    // Chrome based
    on(iframe, 'mozbrowserlocationchange', e => this.onLocationChange(e));
    on(this.els.close, 'click', e => this.onCloseClick(e));
    on(this.els.form, 'submit', e => this.onSubmit(e));
    on(this.els.refresh, 'click', e => this.refresh(e));
    on(this.els.input, 'focus', e => this.onInputFocus(e));

    // Page based
    on(iframe, 'mozbrowsertitlechange', e => this.currentPage.onTitleChange(e));
    on(iframe, 'mozbrowsermetachange', e => this.currentPage.onMetaChange(e));
    on(iframe, 'mozbrowsererror', e => this.currentPage.onError(e));
    on(iframe, 'mozbrowserloadstart', e => this.currentPage.onLoadStart(e));
    on(iframe, 'mozbrowserloadend', e => this.currentPage.onLoadEnd(e));
  },
  currentPage: null,
  sanitizeUrl(url) {
    let outputURL = url;
    ...
    return new URL(outputUrl);
  },
  onSubmit: function (e) {
    e.preventDefault();
    let url = this.els.input.value;
    this.els.input.blur();
    let page = this.setPage(url, defaultContext);
    page.userSubmittedUrl = true;
  },
  onLocationChange: function (e) {
    this.setPage(e.detail);
  },

  shouldResetState(currentPage, newPage) {
    let output = true;
    if (currentPage.origin === newPage.origin) {
      ourput = false;
    }
    return output;
  },

  setPage: function (url) {
    this.url = url;
    let urlObject = this.sanitizeUrl(url);
    // heuristics on resetting the page
    if (this.shouldResetState(urlObject, this.currentPage.url)) {
      this.chrome.setTheme(DEFAULT_THEME);
    }
    this.currentPage = new Page(urlObject, this.chrome);
    return this.currentPage;
  },

  // UI based methods accessible to Pages
  chrome: {
    setTheme(value) {
      scheduler.mutation(() => {
        value = value || '';
        this.els.bar.style.backgroundColor = value;
        this.els.overlay.style.backgroundColor = value;
        this.els.tab.style.backgroundColor = value;
        this.style.backgroundColor = value;
        debug('set theme', value);
      });
    }
  }
});

class Page {
  constructor(urlObject, chrome) {
    this.userSubmittedUrl = false;
    this.url = urlObject;
    this.chrome = chrome;
    this.hasOverrideTheme();
  }
  hasOverrideTheme() {
    const urls = [
      {match: /^https:\/\/mobile[.]twitter[.]com/, color: '#1da1f2'},
      {match: /^https:\/\/m[.]facebook[.]com/, color: '#3a5795'}
    ];
    urls.forEach((url) => {
      if (url.match.test(this.url.origin)) {
        this.theme = url.color;
      }
    });
    return this.theme;
  }
  onMetaChange() {
    var meta = e.detail;

    switch (meta.name) {
      case 'theme-color': this.theme = meta.content; break;
    }
  }

  set theme(color) {
    this._theme = color;
    this.showTheme();
  }
  get theme() {
    return this._theme;
  }

  showTheme() {
    let shownColor = this.theme;
    if (this.securityState === 'insecure') {
      shownColor = 'red';
    }
    if (this.securityState === 'broken') {
      shownColor = DEFAULT_THEME;
    }
    this.chrome.setTheme(shownColor);
  }
  firstPaint() {
    if (!('theme' in this)) {
      this.theme = DEFAULT_THEME;
    }
  }
  error(e) {
    if (this.userSubmittedUrl) {
      // Handle degrade to http
    }
    if (e.detail === 'certError') {
      // ...
      this.showTheme();
    }
  }
  securityChange(e) {
    this.securityState = e.details.state;
    this.showTheme();
  }
}