<commit_before>angular.module('surveysModule' , [])

//
.factory('Surveys' , function($q , $http , $timeout){
     //New Survey schema
     var surveySchema = {
          title:'Generic survey title',
          description:'Describe your survey here',
          target:{
              countries:[],
              interests:[]
          },
          package:{
             type:'',
             responses:'0'
          },
          questioneerId:'',
          answersId:'',
          creator:'palingramblog@gmail.com',
          billingInfo:{
              //@TODO contains info about the payment made
          },
          created:Date.now(), // will be modified on the server
          modified:Date.now(), // will be modified on server
          responses:'0',
          respondents:[],
          campaignStatus:'paused'
     };

     //
     var surveys;
     var activeSurvey;

     //
     function all(user){
          var promise = $q.defer();
          //
          if(!surveys){
              $http({
                  method: 'GET',
                  url:'/survey'+ (user ? '?creator='+user : '')
              })
              .success(function(data){
                   surveys = data;
                   promise.resolve(surveys);
              })
              .error(function(err){
                  promise.reject(err);
              });
          }
          else{
             promise.resolve(surveys);
          }

          return promise.promise;
     }

     //
     function getActive(){
         return activeSurvey;
     }

     //
     function setActive(survey){
         activeSurvey = survey;
     }

     //
     function save(survey){
         var promise = $q.defer();

         $http({
            method: 'POST',
            url: '/survey',
            data: survey
         })
         .success(function(data){
              console.log(data);
               activeSurvey = data;
               if(survey._id){
                   console.log('old');
                   for(var i=0; i<surveys.length; i++){
                       surveys[i]._id == data._id ?
                          surveys[i]=data : '';
                   }
               }
               else{
                  console.log('new');
                  surveys.unshift(data);
               }
               console.log(surveys.length);
               promise.resolve();
         })
         .error(function(err){
              promise.reject(err);
         });

         return promise.promise;
     }

     //
     function initNew(user){
          activeSurvey = surveySchema;
          activeSurvey.creator = user;
          return true;
     }

     //
     return {
        all:all,
        setActive:setActive,
        getActive:getActive,
        save:save,
        initNew : initNew
     };
})

//Abstract state controller for surveys route
.controller('surveysController' , function($scope , $state){
       console.log('surveys abstract controller loaded');
       //Act as a God to all its child-views
})

//
.controller('surveysOverviewController' , function($scope , $state , myMedia , Surveys , profile){
       //
       profile.getUser().then(
          function(user){
            Surveys.all(user.userInfo.email).then(
                 function(data){
                     $scope.surveys = data;
                 },
                 function(err){
                     console.log(err);
                 }
            );
          },
          function(err){
              console.log(err);
          }
       );

       //trasition to /surveys' child view
       $scope.transition = function(survey , view){
            Surveys.setActive(survey);
            console.log(view);

            switch (view) {
              case 'edit':
                 $state.go('dashboard.surveys.edit.setup' , {id:survey._id});
                 break;
              case 'preview':
                 $state.go('dashboard.surveys.preview' , {id:survey._id});
                 break;
              case 'stats':
                 $state.go('dashboard.surveys.stats' , {id:survey._id});
                 break;
            }
       }

       //create a new survey schema and send user to editor
       $scope.createSurvey = function(){
           profile.getUser().then(
               function(user){
                   console.log('Called');
                   Surveys.initNew(user.userInfo.email);
                   $state.go('dashboard.surveys.edit' , {id:1});
               },
               function(err){
                   console.log(err);
               }
           );
       }
})

//
.controller('surveysEditSetupController' , function($scope , $state , Surveys){
    //In case user cancels editing
    var original = Surveys.getActive();

    //
    if(!angular.isDefined(original)){
       $state.go('dashboard.surveys.overview');
    }

    //For use in this scope only
    $scope.survey = angular.copy(original);

    //Setup partial view controls defined here============================//
    $scope.packages = [
         {
            title:'10',
            highlight:false
         },
         {
            title:'20',
            highlight:true
         },
         {
            title:'30',
            highlight:false
         },
    ];

    //
    $scope.setPackage = function(package){
        $scope.survey.package.type = package.title;
        console.log($scope.survey.package.type);
    }

    //
    $scope.invalidPackage = function(){
      if($scope.survey){
          return $scope.survey.package.type == '';
      }
      else{
         return false;
      }
    }

    //
    $scope.invalidTarget = function(){
      if($scope.survey){
          return $scope.survey.target.countries.length ==0 || $scope.survey.target.interests.length==0;
      }
      else{
         return false;
      }
    }

    //
    $scope.activePackageClass = function(package){
         if($scope.survey){
             return $scope.survey.package.type == package.title;
         }
         return '';
    }

    //For country options and interests
    $scope.suggessions = {
        interest:{
            'music':['hiphop' , 'jazz' , 'r&b'],
            'movies':['triller' , 'animation' , 'comedy']
        },
        country:{
             'nigeria':['lagos' , 'porthacourt' , 'kano'],
             'united states':['califonia' , 'newyork']
        }
    };

    //
    $scope.cancel=function(){
         $scope.survey = angular.copy(original);
    }

    //
    $scope.save = function(survey){
         $scope.saving = true;
         Surveys.save(survey).then(
             function(){
                 original = Surveys.getActive();
                 $scope.survey = angular.copy(original);
                 $scope.saving = false;
             },
             function(err){
                 console.log(err);
             }
         );
    }
})

//Questions factory
.factory('Questioneer' , function($q , $http , $timeout){

       //
       var questionSchemas = {
            'Multiple_Choice' : {
                 type:'Multiple_Choice',
                 query:'What is the question again ?',
                 answers:[
                     'Option here',
                     'Option here',
                     'Option here',
                     'Option here'
                 ],
                 answer:-1,
                 options:{
                    multi:false,
                    definite:false
                 }
            }
       };

       //
       function getSchema(schema){
           if(!questionSchemas[schema]){
               alert(schema+' does not exist');
           }
           else{
             return questionSchemas[schema];
           }
       }

       //
       function getQuestions(questioneerId){
            var promise = $q.defer();

            $http({
                method: 'GET',
                url:'/questioneer',
                params:{id:questioneerId}
            })
            .success(function(data){
                 console.log(data);
                 promise.resolve(data);
            })
            .error(function(err){
                 console.log(err);
            });

            return promise.promise;
       }

       //
       function saveQuestioneer(questioneer){
            var promise = $q.defer();
            console.log(questioneer);
            $http({
                method: 'POST',
                url:'/questioneer',
                data:questioneer
            })
            .success(function(data){
                console.log(data);
                promise.resolve(true);
            })
            .error(function(err){
                 console.log(err);
                 promise.reject(err);
            });

            return promise.promise;
       }

       //
       return {
           getSchema : getSchema,
           getQuestions:getQuestions,
           saveQuestioneer:saveQuestioneer
       }
})

//
.controller('surveysEditBuilderController' , function($scope , $state, $timeout,  Surveys , Questioneer , Spinners , alertService){
      //In case user cancels editing
      $scope.survey = Surveys.getActive();

      //
      var original;
      if(!angular.isDefined($scope.survey)){
         $state.go('dashboard.surveys.overview');
      }
      else{
          Questioneer.getQuestions($scope.survey.questioneerId).then(
              function(data){
                  original = data;
                  $scope.questioneer = angular.copy(original);
              },
              function(err){
                  console.log(err);
              }
          );
      }

      //This triggers this view to create a new question when user click on a type in the sidemenu
      $scope.createQuestion = function(type){
          $scope.questioneer.questions.push(angular.copy(Questioneer.getSchema(type)));
      }

      //this is triggered when a question is saved
      $scope.saveQuestioneer = function(spinnerName){
          Questioneer.saveQuestioneer($scope.questioneer).then(
              function(status){
                  console.log(status);
                  Spinners.spinner(spinnerName).hide();
                  alertService.alert({msg:'Question saved successfully' , class:'success'});

              },
              function(err){
                  Spinners.spinner(spinnerName).error();
                  alertService.alert({msg:'Question not saved' , class:'danger'});
              }
          );
      }

      //This is the callback triggered by the question types directive
      $scope.removeQuestion = function(index , spinnerName){
          var temp = angular.copy($scope.questioneer);
          temp.questions.splice(index , 1);
          Questioneer.saveQuestioneer(temp).then(
              function(status){
                  console.log(status);
                  $scope.questioneer = angular.copy(temp);
                  temp = undefined;
                  Spinners.spinner(spinnerName).hide();
              },
              function(err){
                  Spinners.spinner(spinnerName).error();
              }
          );
      }
})

//
.controller('surveysEditBillingController' , function($scope , $state, Surveys){
      //In case user cancels editing
      var original = Surveys.getActive();

      //
      if(!angular.isDefined(original)){
         $state.go('dashboard.surveys.overview');
      }

      //For use in this scope only
      $scope.survey = angular.copy(original);
})

//
.controller('surveysPreviewController' , function($scope , $state, $stateParams){
       $scope.hello = 'surveys/preview '+$stateParams.id;
})

//
.controller('surveysStatsController' , function($scope , $state, $stateParams){
       $scope.hello = 'surveys/stats '+$stateParams.id;
})
<commit_msg>Signed-off-by: codemuhammed <codemuhammed@gmail.com><commit_after>angular.module('surveysModule' , [])

//
.factory('Surveys' , function($q , $http , $timeout){
     //New Survey schema
     var surveySchema = {
          title:'Generic survey title',
          description:'Describe your survey here',
          target:{
              countries:[],
              interests:[]
          },
          package:{
             type:'',
             responses:'0'
          },
          questioneerId:'',
          answersId:'',
          creator:'palingramblog@gmail.com',
          billingInfo:{
              //@TODO contains info about the payment made
          },
          created:Date.now(), // will be modified on the server
          modified:Date.now(), // will be modified on server
          responses:'0',
          respondents:[],
          campaignStatus:'paused'
     };

     //
     var surveys;
     var activeSurvey;

     //
     function all(user){
          var promise = $q.defer();
          //
          if(!surveys){
              $http({
                  method: 'GET',
                  url:'/survey'+ (user ? '?creator='+user : '')
              })
              .success(function(data){
                   surveys = data;
                   promise.resolve(surveys);
              })
              .error(function(err){
                  promise.reject(err);
              });
          }
          else{
             promise.resolve(surveys);
          }

          return promise.promise;
     }

     //
     function getActive(){
         return activeSurvey;
     }

     //
     function setActive(survey){
         activeSurvey = survey;
     }

     //
     function save(survey){
         var promise = $q.defer();

         $http({
            method: 'POST',
            url: '/survey',
            data: survey
         })
         .success(function(data){
              console.log(data);
               activeSurvey = data;
               if(survey._id){
                   console.log('old');
                   for(var i=0; i<surveys.length; i++){
                       surveys[i]._id == data._id ?
                          surveys[i]=data : '';
                   }
               }
               else{
                  console.log('new');
                  surveys.unshift(data);
               }
               console.log(surveys.length);
               promise.resolve();
         })
         .error(function(err){
              promise.reject(err);
         });

         return promise.promise;
     }

     //
     function initNew(user){
          activeSurvey = surveySchema;
          activeSurvey.creator = user;
          return true;
     }

     //
     return {
        all:all,
        setActive:setActive,
        getActive:getActive,
        save:save,
        initNew : initNew
     };
})

//Abstract state controller for surveys route
.controller('surveysController' , function($scope , $state){
       console.log('surveys abstract controller loaded');
       //Act as a God to all its child-views
})

//
.controller('surveysOverviewController' , function($scope , $state , myMedia , Surveys , profile){
       //
       profile.getUser().then(
          function(user){
            Surveys.all(user.userInfo.email).then(
                 function(data){
                     $scope.surveys = data;
                 },
                 function(err){
                     console.log(err);
                 }
            );
          },
          function(err){
              console.log(err);
          }
       );

       //trasition to /surveys' child view
       $scope.transition = function(survey , view){
            Surveys.setActive(survey);
            console.log(view);

            switch (view) {
              case 'edit':
                 $state.go('dashboard.surveys.edit.setup' , {id:survey._id});
                 break;
              case 'preview':
                 $state.go('dashboard.surveys.preview' , {id:survey._id});
                 break;
              case 'stats':
                 $state.go('dashboard.surveys.stats' , {id:survey._id});
                 break;
            }
       }

       //create a new survey schema and send user to editor
       $scope.createSurvey = function(){
           profile.getUser().then(
               function(user){
                   console.log('Called');
                   Surveys.initNew(user.userInfo.email);
                   $state.go('dashboard.surveys.edit' , {id:1});
               },
               function(err){
                   console.log(err);
               }
           );
       }
})

//
.controller('surveysEditSetupController' , function($scope , $state , Surveys , alertService){
    //In case user cancels editing
    var original = Surveys.getActive();

    //
    if(!angular.isDefined(original)){
       $state.go('dashboard.surveys.overview');
    }

    //For use in this scope only
    $scope.survey = angular.copy(original);

    //Setup partial view controls defined here============================//
    $scope.packages = [
         {
            title:'10',
            highlight:false
         },
         {
            title:'20',
            highlight:true
         },
         {
            title:'30',
            highlight:false
         },
    ];

    //
    $scope.setPackage = function(package){
        $scope.survey.package.type = package.title;
        console.log($scope.survey.package.type);
    }

    //
    $scope.invalidPackage = function(){
      if($scope.survey){
          return $scope.survey.package.type == '';
      }
      else{
         return false;
      }
    }

    //
    $scope.invalidTarget = function(){
      if($scope.survey){
          return $scope.survey.target.countries.length ==0 || $scope.survey.target.interests.length==0;
      }
      else{
         return false;
      }
    }

    //
    $scope.activePackageClass = function(package){
         if($scope.survey){
             return $scope.survey.package.type == package.title;
         }
         return '';
    }

    //For country options and interests
    $scope.suggessions = {
        interest:{
            'music':['hiphop' , 'jazz' , 'r&b'],
            'movies':['triller' , 'animation' , 'comedy']
        },
        country:{
             'nigeria':['lagos' , 'porthacourt' , 'kano'],
             'united states':['califonia' , 'newyork']
        }
    };

    //
    $scope.cancel=function(){
         $scope.survey = angular.copy(original);
    }

    //
    $scope.save = function(survey){
         $scope.saving = true;
         Surveys.save(survey).then(
             function(){
                 original = Surveys.getActive();
                 $scope.survey = angular.copy(original);
                 $scope.saving = false;
                 alertService.alert({msg:'Question saved successfully' , class:'success'});
             },
             function(err){
                 console.log(err);
                 alertService.alert({msg:'Error saving data' , class:'danger'});
             }
         );
    }
})

//Questions factory
.factory('Questioneer' , function($q , $http , $timeout){

       //
       var questionSchemas = {
            'Multiple_Choice' : {
                 type:'Multiple_Choice',
                 query:'What is the question again ?',
                 answers:[
                     'Option here',
                     'Option here',
                     'Option here',
                     'Option here'
                 ],
                 answer:-1,
                 options:{
                    multi:false,
                    definite:false
                 }
            }
       };

       //
       function getSchema(schema){
           if(!questionSchemas[schema]){
               alert(schema+' does not exist');
           }
           else{
             return questionSchemas[schema];
           }
       }

       //
       function getQuestions(questioneerId){
            var promise = $q.defer();

            $http({
                method: 'GET',
                url:'/questioneer',
                params:{id:questioneerId}
            })
            .success(function(data){
                 console.log(data);
                 promise.resolve(data);
            })
            .error(function(err){
                 console.log(err);
            });

            return promise.promise;
       }

       //
       function saveQuestioneer(questioneer){
            var promise = $q.defer();
            console.log(questioneer);
            $http({
                method: 'POST',
                url:'/questioneer',
                data:questioneer
            })
            .success(function(data){
                console.log(data);
                promise.resolve(true);
            })
            .error(function(err){
                 console.log(err);
                 promise.reject(err);
            });

            return promise.promise;
       }

       //
       return {
           getSchema : getSchema,
           getQuestions:getQuestions,
           saveQuestioneer:saveQuestioneer
       }
})

//
.controller('surveysEditBuilderController' , function($scope , $state, $timeout,  Surveys , Questioneer , Spinners , alertService){
      //In case user cancels editing
      $scope.survey = Surveys.getActive();

      //
      var original;
      if(!angular.isDefined($scope.survey)){
         $state.go('dashboard.surveys.overview');
      }
      else{
          Questioneer.getQuestions($scope.survey.questioneerId).then(
              function(data){
                  original = data;
                  $scope.questioneer = angular.copy(original);
              },
              function(err){
                  console.log(err);
              }
          );
      }

      //This triggers this view to create a new question when user click on a type in the sidemenu
      $scope.createQuestion = function(type){
          $scope.questioneer.questions.push(angular.copy(Questioneer.getSchema(type)));
      }

      //this is triggered when a question is saved
      $scope.saveQuestioneer = function(spinnerName){
          Questioneer.saveQuestioneer($scope.questioneer).then(
              function(status){
                  console.log(status);
                  Spinners.spinner(spinnerName).hide();
                  alertService.alert({msg:'Question saved successfully' , class:'success'});

              },
              function(err){
                  Spinners.spinner(spinnerName).error();
                  alertService.alert({msg:'Question not saved' , class:'danger'});
              }
          );
      }

      //This is the callback triggered by the question types directive
      $scope.removeQuestion = function(index , spinnerName){
          var temp = angular.copy($scope.questioneer);
          temp.questions.splice(index , 1);
          Questioneer.saveQuestioneer(temp).then(
              function(status){
                  console.log(status);
                  $scope.questioneer = angular.copy(temp);
                  temp = undefined;
                  Spinners.spinner(spinnerName).hide();
              },
              function(err){
                  Spinners.spinner(spinnerName).error();
              }
          );
      }
})

//
.controller('surveysEditBillingController' , function($scope , $state, Surveys){
      //In case user cancels editing
      var original = Surveys.getActive();

      //
      if(!angular.isDefined(original)){
         $state.go('dashboard.surveys.overview');
      }

      //For use in this scope only
      $scope.survey = angular.copy(original);
})

//
.controller('surveysPreviewController' , function($scope , $state, $stateParams){
       $scope.hello = 'surveys/preview '+$stateParams.id;
})

//
.controller('surveysStatsController' , function($scope , $state, $stateParams){
       $scope.hello = 'surveys/stats '+$stateParams.id;
})
<|endoftext|>


<commit_before>/* @flow strict-local */
import React, { Component } from 'react';
import { Platform } from 'react-native';
import { WebView } from 'react-native-webview';
import type { WebViewNavigation } from 'react-native-webview';

import { connectActionSheet } from '@expo/react-native-action-sheet';

import type {
  AlertWordsState,
  Auth,
  Context,
  Debug,
  Dispatch,
  Fetching,
  FlagsState,
  GetText,
  Message,
  MuteState,
  Narrow,
  Outbox,
  ImageEmojiType,
  RenderedSectionDescriptor,
  Subscription,
  ThemeName,
  User,
} from '../types';
import { connect } from '../react-redux';
import {
  getAuth,
  getAllImageEmojiById,
  getCurrentTypingUsers,
  getDebug,
  getRenderedMessages,
  getFlags,
  getAnchorForNarrow,
  getFetchingForNarrow,
  getMute,
  getOwnEmail,
  getOwnUser,
  getSettings,
  getSubscriptions,
  getShowMessagePlaceholders,
  getShownMessagesForNarrow,
  getRealm,
} from '../selectors';
import { withGetText } from '../boot/TranslationProvider';

import type { ShowActionSheetWithOptions } from '../message/messageActionSheet';
import type { WebViewUpdateEvent } from './webViewHandleUpdates';
import type { MessageListEvent } from './webViewEventHandlers';
import getHtml from './html/html';
import renderMessagesAsHtml from './html/renderMessagesAsHtml';
import { getUpdateEvents } from './webViewHandleUpdates';
import { handleMessageListEvent } from './webViewEventHandlers';
import { base64Utf8Encode } from '../utils/encoding';
import * as logging from '../utils/logging';

// ESLint doesn't notice how `this.props` escapes, and complains about some
// props not being used here.
/* eslint-disable react/no-unused-prop-types */

/**
 * Data about the user, the realm, and all known messages.
 *
 * This data is all independent of the specific narrow or specific messages
 * we're displaying; data about those goes elsewhere.
 */
export type BackgroundData = $ReadOnly<{
  alertWords: AlertWordsState,
  auth: Auth,
  debug: Debug,
  flags: FlagsState,
  mute: MuteState,
  ownEmail: string,
  ownUserId: number,
  allImageEmojiById: $ReadOnly<{ [id: string]: ImageEmojiType }>,
  twentyFourHourTime: boolean,
  subscriptions: Subscription[],
}>;

type SelectorProps = {|
  backgroundData: BackgroundData,
  anchor: number,
  fetching: Fetching,
  messages: $ReadOnlyArray<Message | Outbox>,
  renderedMessages: RenderedSectionDescriptor[],
  showMessagePlaceholders: boolean,
  theme: ThemeName,
  typingUsers: $ReadOnlyArray<User>,
|};

// TODO get a type for `connectActionSheet` so this gets fully type-checked.
export type Props = $ReadOnly<{|
  narrow: Narrow,

  dispatch: Dispatch,
  ...SelectorProps,

  // From `connectActionSheet`.
  showActionSheetWithOptions: ShowActionSheetWithOptions,

  // From `withGetText`.
  _: GetText,
|}>;

/**
 * A URL-like magic string denoting the webview-assets folder.
 *
 * Not suitable for reuse or export.
 */
const assetsPath = Platform.OS === 'ios' ? './webview' : 'file:///android_asset/webview';
// What the value above probably _should_ be, semantically, is an absolute
// `file:`-scheme URL naming the path to the webview-assets folder. [1]
//
// * On Android, that's exactly what it is. Different apps' WebViews see
//   different (virtual) root directories as `file:///`, and at runtime, the
//   APK's `assets/` directory is mounted at `file:///android_asset/`. We can
//   easily hardcode that, so we do.
//
// * On iOS, it's not: it's a relative path. (Or relative URL, if you prefer.)
//   We can't make it absolute here, because neither React Native itself nor any
//   of our current dependencies directly expose the Foundation API that would
//   tell us the absolute path that our bundle is located at [2].
//
//   Instead, for now, we exploit the fact that (the iOS version of) React
//   Native will resolve it with respect to the bundle's absolute path. [3] This
//   fact is not known to be documented, however, and should not be taken for
//   granted indefinitely.
//
// [1] See `tools/build-webview` for more information on what folder that is.
//
// [2] Specifically, `Bundle.main.bundleURL` (aka `[[NSBundle mainbundle]
//     bundleURL]`). Alternatively, the value of `resourceURL` is (believed to
//     be) equivalent in this context.
//
// [3] https://github.com/facebook/react-native/blob/0.59-stable/React/Base/RCTConvert.m#L85

class MessageList extends Component<Props> {
  context: Context;
  webview: ?WebView;
  sendUpdateEventsIsReady: boolean;
  unsentUpdateEvents: WebViewUpdateEvent[] = [];

  static contextTypes = {
    styles: () => null,
    theme: () => null,
  };

  componentDidMount() {
    this.setupSendUpdateEvents();
  }

  handleError = (event: mixed) => {
    console.error(event); // eslint-disable-line
  };

  /**
   * Initiate round-trip handshakes with the WebView, until one succeeds.
   */
  setupSendUpdateEvents = (): void => {
    const intervalId = setInterval(() => {
      if (!this.sendUpdateEventsIsReady) {
        this.sendUpdateEvents([{ type: 'ready' }]);
      } else {
        clearInterval(intervalId);
      }
    }, 30);
  };

  sendUpdateEvents = (uevents: WebViewUpdateEvent[]): void => {
    if (this.webview && uevents.length > 0) {
      // $FlowFixMe This `postMessage` is undocumented; tracking as #3572.
      const secretWebView: { postMessage: (string, string) => void } = this.webview;
      secretWebView.postMessage(base64Utf8Encode(JSON.stringify(uevents)), '*');
    }
  };

  handleMessage = (event: { +nativeEvent: { +data: string } }) => {
    const eventData: MessageListEvent = JSON.parse(event.nativeEvent.data);
    if (eventData.type === 'ready') {
      this.sendUpdateEventsIsReady = true;
      this.sendUpdateEvents(this.unsentUpdateEvents);
    } else {
      const { _ } = this.props;
      handleMessageListEvent(this.props, _, eventData);
    }
  };

  shouldComponentUpdate = (nextProps: Props) => {
    const uevents = getUpdateEvents(this.props, nextProps);

    if (this.sendUpdateEventsIsReady) {
      this.sendUpdateEvents(uevents);
    } else {
      this.unsentUpdateEvents.push(...uevents);
    }

    return false;
  };

  render() {
    const { styles: contextStyles } = this.context;
    const {
      backgroundData,
      renderedMessages,
      anchor,
      narrow,
      theme,
      showMessagePlaceholders,
    } = this.props;
    const messagesHtml = renderMessagesAsHtml(backgroundData, narrow, renderedMessages);
    const { auth } = backgroundData;
    const html = getHtml(messagesHtml, theme, {
      anchor,
      auth,
      showMessagePlaceholders,
    });

    /**
     * Effective URL of the MessageList webview.
     *
     * We provide all HTML at creation (or refresh) time; the document named
     * here is not loaded, and doesn't even need to exist. It serves only as a
     * placeholder, so that relative URLs and cross-domain security restrictions
     * have somewhere to believe that this document originates from.
     */
    const baseUrl = `${assetsPath}/index.html`;

    // Paranoia^WSecurity: only load `baseUrl`, and only load it once. Any other
    // requests should be handed off to the OS, not loaded inside the WebView.
    const onShouldStartLoadWithRequest: (event: WebViewNavigation) => boolean = (() => {
      // Inner closure to actually test the URL.
      const urlTester: (url: string) => boolean = (() => {
        // On Android this function is documented to be skipped on first load:
        // therefore, simply never return true.
        if (Platform.OS === 'android') {
          return (url: string) => false;
        }

        // Otherwise (for iOS), return a closure that evaluates to `true` _exactly
        // once_, and even then only if the URL looks like what we're expecting.
        let loaded_once = false;
        // The baseUrl, with its relative portion (if any) stripped.
        const baseUrlTail = baseUrl.replace(/^\.\//, '');
        // Disallow such monstrosities as `evilsite.com/?./webview/index.html`.
        const unsafeUrlRegex = /[&?]/;
        return (url: string) => {
          if (!loaded_once) {
            // The exact URL that will be loaded could be determined statically on
            // Android. On iOS, though, it involves some unpredictable UUIDs which
            // RN provides no good interface to. (`react-native-fs` is awful.)
            if (
              url.startsWith('file://')
              && url.endsWith(baseUrlTail)
              && !unsafeUrlRegex.test(url)
            ) {
              loaded_once = true;
              return true;
            }
          }
          return false;
        };
      })();

      // Outer closure to perform logging.
      return (event: WebViewNavigation) => {
        const ok = urlTester(event.url);
        if (!ok) {
          logging.warn('webview: rejected navigation event', {
            navigation_event: { ...event },
            expected_url: baseUrl,
          });
        }
        return ok;
      };
    })();

    // The `originWhitelist` and `onShouldStartLoadWithRequest` props are
    // meant to mitigate possible XSS bugs, by interrupting an attempted
    // exploit if it tries to navigate to a new URL by e.g. setting
    // `window.location`.
    //
    // Note that neither of them is a hard security barrier; they're checked
    // only against the URL of the document itself.  They cannot be used to
    // validate the URL of other resources the WebView loads.
    //
    // Worse, the `originWhitelist` parameter is completely broken. See:
    // https://github.com/react-native-community/react-native-webview/pull/697
    return (
      <WebView
        useWebKit
        source={{ baseUrl, html }}
        originWhitelist={['file://']}
        onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
        style={contextStyles.webview}
        ref={webview => {
          this.webview = webview;
        }}
        onMessage={this.handleMessage}
        onError={this.handleError}
      />
    );
  }
}

type OuterProps = {|
  narrow: Narrow,

  /* Remaining props are derived from `narrow` by default. */

  messages?: Message[],
  renderedMessages?: RenderedSectionDescriptor[],
  anchor?: number,

  /* Passing these three from the parent is kind of a hack; search uses it
     to hard-code some behavior. */
  fetching?: Fetching,
  showMessagePlaceholders?: boolean,
  typingUsers?: User[],
|};

export default connect<SelectorProps, _, _>((state, props: OuterProps) => {
  // TODO Ideally this ought to be a caching selector that doesn't change
  // when the inputs don't.  Doesn't matter in a practical way here, because
  // we have a `shouldComponentUpdate` that doesn't look at this prop... but
  // it'd be better to set an example of the right general pattern.
  const backgroundData: BackgroundData = {
    alertWords: state.alertWords,
    auth: getAuth(state),
    debug: getDebug(state),
    flags: getFlags(state),
    mute: getMute(state),
    ownEmail: getOwnEmail(state),
    ownUserId: getOwnUser(state).user_id,
    allImageEmojiById: getAllImageEmojiById(state),
    subscriptions: getSubscriptions(state),
    twentyFourHourTime: getRealm(state).twentyFourHourTime,
  };

  return {
    backgroundData,
    anchor: props.anchor !== undefined ? props.anchor : getAnchorForNarrow(props.narrow)(state),
    fetching: props.fetching || getFetchingForNarrow(props.narrow)(state),
    messages: props.messages || getShownMessagesForNarrow(state, props.narrow),
    renderedMessages: props.renderedMessages || getRenderedMessages(props.narrow)(state),
    showMessagePlaceholders:
      props.showMessagePlaceholders !== undefined
        ? props.showMessagePlaceholders
        : getShowMessagePlaceholders(props.narrow)(state),
    theme: getSettings(state).theme,
    typingUsers: props.typingUsers || getCurrentTypingUsers(state, props.narrow),
  };
})(connectActionSheet(withGetText(MessageList)));
<commit_msg>MessageList: Fix white flicker in dark mode on iOS while WebView is initializing.<commit_after>/* @flow strict-local */
import React, { Component } from 'react';
import { Platform, View } from 'react-native';
import { WebView } from 'react-native-webview';
import type { WebViewNavigation } from 'react-native-webview';

import { connectActionSheet } from '@expo/react-native-action-sheet';

import type {
  AlertWordsState,
  Auth,
  Context,
  Debug,
  Dispatch,
  Fetching,
  FlagsState,
  GetText,
  Message,
  MuteState,
  Narrow,
  Outbox,
  ImageEmojiType,
  RenderedSectionDescriptor,
  Subscription,
  ThemeName,
  User,
} from '../types';
import { connect } from '../react-redux';
import {
  getAuth,
  getAllImageEmojiById,
  getCurrentTypingUsers,
  getDebug,
  getRenderedMessages,
  getFlags,
  getAnchorForNarrow,
  getFetchingForNarrow,
  getMute,
  getOwnEmail,
  getOwnUser,
  getSettings,
  getSubscriptions,
  getShowMessagePlaceholders,
  getShownMessagesForNarrow,
  getRealm,
} from '../selectors';
import { withGetText } from '../boot/TranslationProvider';

import type { ShowActionSheetWithOptions } from '../message/messageActionSheet';
import type { WebViewUpdateEvent } from './webViewHandleUpdates';
import type { MessageListEvent } from './webViewEventHandlers';
import getHtml from './html/html';
import renderMessagesAsHtml from './html/renderMessagesAsHtml';
import { getUpdateEvents } from './webViewHandleUpdates';
import { handleMessageListEvent } from './webViewEventHandlers';
import { base64Utf8Encode } from '../utils/encoding';
import * as logging from '../utils/logging';

// ESLint doesn't notice how `this.props` escapes, and complains about some
// props not being used here.
/* eslint-disable react/no-unused-prop-types */

/**
 * Data about the user, the realm, and all known messages.
 *
 * This data is all independent of the specific narrow or specific messages
 * we're displaying; data about those goes elsewhere.
 */
export type BackgroundData = $ReadOnly<{
  alertWords: AlertWordsState,
  auth: Auth,
  debug: Debug,
  flags: FlagsState,
  mute: MuteState,
  ownEmail: string,
  ownUserId: number,
  allImageEmojiById: $ReadOnly<{ [id: string]: ImageEmojiType }>,
  twentyFourHourTime: boolean,
  subscriptions: Subscription[],
}>;

type SelectorProps = {|
  backgroundData: BackgroundData,
  anchor: number,
  fetching: Fetching,
  messages: $ReadOnlyArray<Message | Outbox>,
  renderedMessages: RenderedSectionDescriptor[],
  showMessagePlaceholders: boolean,
  theme: ThemeName,
  typingUsers: $ReadOnlyArray<User>,
|};

// TODO get a type for `connectActionSheet` so this gets fully type-checked.
export type Props = $ReadOnly<{|
  narrow: Narrow,

  dispatch: Dispatch,
  ...SelectorProps,

  // From `connectActionSheet`.
  showActionSheetWithOptions: ShowActionSheetWithOptions,

  // From `withGetText`.
  _: GetText,
|}>;

/**
 * A URL-like magic string denoting the webview-assets folder.
 *
 * Not suitable for reuse or export.
 */
const assetsPath = Platform.OS === 'ios' ? './webview' : 'file:///android_asset/webview';
// What the value above probably _should_ be, semantically, is an absolute
// `file:`-scheme URL naming the path to the webview-assets folder. [1]
//
// * On Android, that's exactly what it is. Different apps' WebViews see
//   different (virtual) root directories as `file:///`, and at runtime, the
//   APK's `assets/` directory is mounted at `file:///android_asset/`. We can
//   easily hardcode that, so we do.
//
// * On iOS, it's not: it's a relative path. (Or relative URL, if you prefer.)
//   We can't make it absolute here, because neither React Native itself nor any
//   of our current dependencies directly expose the Foundation API that would
//   tell us the absolute path that our bundle is located at [2].
//
//   Instead, for now, we exploit the fact that (the iOS version of) React
//   Native will resolve it with respect to the bundle's absolute path. [3] This
//   fact is not known to be documented, however, and should not be taken for
//   granted indefinitely.
//
// [1] See `tools/build-webview` for more information on what folder that is.
//
// [2] Specifically, `Bundle.main.bundleURL` (aka `[[NSBundle mainbundle]
//     bundleURL]`). Alternatively, the value of `resourceURL` is (believed to
//     be) equivalent in this context.
//
// [3] https://github.com/facebook/react-native/blob/0.59-stable/React/Base/RCTConvert.m#L85

class MessageList extends Component<Props> {
  context: Context;
  webview: ?WebView;
  sendUpdateEventsIsReady: boolean;
  unsentUpdateEvents: WebViewUpdateEvent[] = [];

  static contextTypes = {
    styles: () => null,
    theme: () => null,
  };

  componentDidMount() {
    this.setupSendUpdateEvents();
  }

  handleError = (event: mixed) => {
    console.error(event); // eslint-disable-line
  };

  /**
   * Initiate round-trip handshakes with the WebView, until one succeeds.
   */
  setupSendUpdateEvents = (): void => {
    const intervalId = setInterval(() => {
      if (!this.sendUpdateEventsIsReady) {
        this.sendUpdateEvents([{ type: 'ready' }]);
      } else {
        clearInterval(intervalId);
      }
    }, 30);
  };

  sendUpdateEvents = (uevents: WebViewUpdateEvent[]): void => {
    if (this.webview && uevents.length > 0) {
      // $FlowFixMe This `postMessage` is undocumented; tracking as #3572.
      const secretWebView: { postMessage: (string, string) => void } = this.webview;
      secretWebView.postMessage(base64Utf8Encode(JSON.stringify(uevents)), '*');
    }
  };

  handleMessage = (event: { +nativeEvent: { +data: string } }) => {
    const eventData: MessageListEvent = JSON.parse(event.nativeEvent.data);
    if (eventData.type === 'ready') {
      this.sendUpdateEventsIsReady = true;
      this.sendUpdateEvents(this.unsentUpdateEvents);
    } else {
      const { _ } = this.props;
      handleMessageListEvent(this.props, _, eventData);
    }
  };

  shouldComponentUpdate = (nextProps: Props) => {
    const uevents = getUpdateEvents(this.props, nextProps);

    if (this.sendUpdateEventsIsReady) {
      this.sendUpdateEvents(uevents);
    } else {
      this.unsentUpdateEvents.push(...uevents);
    }

    return false;
  };

  renderLoading = () => {
    const style = {
      backgroundColor: 'transparent',
      width: '100%',
      height: '100%',
    };
    return <View style={style} />;
  };

  render() {
    const { styles: contextStyles } = this.context;
    const {
      backgroundData,
      renderedMessages,
      anchor,
      narrow,
      theme,
      showMessagePlaceholders,
    } = this.props;
    const messagesHtml = renderMessagesAsHtml(backgroundData, narrow, renderedMessages);
    const { auth } = backgroundData;
    const html = getHtml(messagesHtml, theme, {
      anchor,
      auth,
      showMessagePlaceholders,
    });

    /**
     * Effective URL of the MessageList webview.
     *
     * We provide all HTML at creation (or refresh) time; the document named
     * here is not loaded, and doesn't even need to exist. It serves only as a
     * placeholder, so that relative URLs and cross-domain security restrictions
     * have somewhere to believe that this document originates from.
     */
    const baseUrl = `${assetsPath}/index.html`;

    // Paranoia^WSecurity: only load `baseUrl`, and only load it once. Any other
    // requests should be handed off to the OS, not loaded inside the WebView.
    const onShouldStartLoadWithRequest: (event: WebViewNavigation) => boolean = (() => {
      // Inner closure to actually test the URL.
      const urlTester: (url: string) => boolean = (() => {
        // On Android this function is documented to be skipped on first load:
        // therefore, simply never return true.
        if (Platform.OS === 'android') {
          return (url: string) => false;
        }

        // Otherwise (for iOS), return a closure that evaluates to `true` _exactly
        // once_, and even then only if the URL looks like what we're expecting.
        let loaded_once = false;
        // The baseUrl, with its relative portion (if any) stripped.
        const baseUrlTail = baseUrl.replace(/^\.\//, '');
        // Disallow such monstrosities as `evilsite.com/?./webview/index.html`.
        const unsafeUrlRegex = /[&?]/;
        return (url: string) => {
          if (!loaded_once) {
            // The exact URL that will be loaded could be determined statically on
            // Android. On iOS, though, it involves some unpredictable UUIDs which
            // RN provides no good interface to. (`react-native-fs` is awful.)
            if (
              url.startsWith('file://')
              && url.endsWith(baseUrlTail)
              && !unsafeUrlRegex.test(url)
            ) {
              loaded_once = true;
              return true;
            }
          }
          return false;
        };
      })();

      // Outer closure to perform logging.
      return (event: WebViewNavigation) => {
        const ok = urlTester(event.url);
        if (!ok) {
          logging.warn('webview: rejected navigation event', {
            navigation_event: { ...event },
            expected_url: baseUrl,
          });
        }
        return ok;
      };
    })();

    // The `originWhitelist` and `onShouldStartLoadWithRequest` props are
    // meant to mitigate possible XSS bugs, by interrupting an attempted
    // exploit if it tries to navigate to a new URL by e.g. setting
    // `window.location`.
    //
    // Note that neither of them is a hard security barrier; they're checked
    // only against the URL of the document itself.  They cannot be used to
    // validate the URL of other resources the WebView loads.
    //
    // Worse, the `originWhitelist` parameter is completely broken. See:
    // https://github.com/react-native-community/react-native-webview/pull/697
    return (
      <WebView
        useWebKit
        startInLoadingState
        renderLoading={this.renderLoading}
        source={{ baseUrl, html }}
        originWhitelist={['file://']}
        onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
        style={contextStyles.webview}
        ref={webview => {
          this.webview = webview;
        }}
        onMessage={this.handleMessage}
        onError={this.handleError}
      />
    );
  }
}

type OuterProps = {|
  narrow: Narrow,

  /* Remaining props are derived from `narrow` by default. */

  messages?: Message[],
  renderedMessages?: RenderedSectionDescriptor[],
  anchor?: number,

  /* Passing these three from the parent is kind of a hack; search uses it
     to hard-code some behavior. */
  fetching?: Fetching,
  showMessagePlaceholders?: boolean,
  typingUsers?: User[],
|};

export default connect<SelectorProps, _, _>((state, props: OuterProps) => {
  // TODO Ideally this ought to be a caching selector that doesn't change
  // when the inputs don't.  Doesn't matter in a practical way here, because
  // we have a `shouldComponentUpdate` that doesn't look at this prop... but
  // it'd be better to set an example of the right general pattern.
  const backgroundData: BackgroundData = {
    alertWords: state.alertWords,
    auth: getAuth(state),
    debug: getDebug(state),
    flags: getFlags(state),
    mute: getMute(state),
    ownEmail: getOwnEmail(state),
    ownUserId: getOwnUser(state).user_id,
    allImageEmojiById: getAllImageEmojiById(state),
    subscriptions: getSubscriptions(state),
    twentyFourHourTime: getRealm(state).twentyFourHourTime,
  };

  return {
    backgroundData,
    anchor: props.anchor !== undefined ? props.anchor : getAnchorForNarrow(props.narrow)(state),
    fetching: props.fetching || getFetchingForNarrow(props.narrow)(state),
    messages: props.messages || getShownMessagesForNarrow(state, props.narrow),
    renderedMessages: props.renderedMessages || getRenderedMessages(props.narrow)(state),
    showMessagePlaceholders:
      props.showMessagePlaceholders !== undefined
        ? props.showMessagePlaceholders
        : getShowMessagePlaceholders(props.narrow)(state),
    theme: getSettings(state).theme,
    typingUsers: props.typingUsers || getCurrentTypingUsers(state, props.narrow),
  };
})(connectActionSheet(withGetText(MessageList)));
<|endoftext|>


<commit_before>Ext.ns('Garp.renderers');

/**
 * General Column Renderers & Field converters
 *
 */
Ext.apply(Garp.renderers,{

	/**
	 * Converter for fullname fields. Combines first, prefix, last into one field. Usefull e.g. as displayField for relationFields
	 * @param {Object} v field value (not used)
	 * @param {Object} rec Ext.Record
	 */
	fullNameConverter: function(v, rec){
		var name = [];
		Ext.each(['first_name', 'last_name_prefix', 'last_name'], function(f){
			if(rec[f]){
				name.push(rec[f]);
			}
		});
		if(!name){
			name = rec.name;
		}
		return name.join(' ');
	},

	/**
	 * Google address resolver. Forms only
	 * @param {Object} arr
	 * @return {String} formated address
	 */
	geocodeAddressRenderer: function(arr){
		arr = arr.address_components;
		var country_long,
		country,
		city,
		street,
		housenumber,
		out = '';

		Ext.each(arr, function(i){
			if(i.types.indexOf('country') > -1){
				country_long = i.long_name;
				country = i.short_name;
			}
			if(i.types.indexOf('locality') > -1){
				city = i.short_name;
			}
			if(i.types.indexOf('route') > -1){
				street = i.short_name;
			}
			if(i.types.indexOf('street_number') > -1){
				housenumber = i.short_name;
			}

		});
		if (housenumber) {
			out = street + ' ' + housenumber;
		} else if (street) {
			out = street;
		}
		if(city){
			out += ', ' + city;
		}
		if(country != 'NL'){
			out += ', ' + country_long;
		}
		return out;
	},

	/**
	 * We do not want HTML to be viewed in a grid column. This causes significant performance hogs and Adobe Flash™ bugs otherwise...
	 */
	htmlRenderer: function(){
		return '';
	},

	/**
	 * Shorter Date & Time
	 * @param {Date/String} date
	 */
	shortDateTimeRenderer: function(date, meta, rec){
		var displayFormat = 'd M Y H:i';
		if (Ext.isDate(date)) {
			return date.format(displayFormat);
		}
		if (date && typeof Date.parseDate(date,'Y-m-d H:i:s') != 'undefined') {
			return Date.parseDate(date, 'Y-m-d H:i:s').format(displayFormat);
		}
		return '-';
	},

	/**
	 * Date & Time
	 * @param {Date/String} date
	 */
	dateTimeRenderer: function(date, meta, rec){
		var displayFormat = 'd F Y H:i';
		if (Ext.isDate(date)) {
			return date.format(displayFormat);
		}
		if (date && typeof Date.parseDate(date,'Y-m-d H:i:s') != 'undefined') {
			return Date.parseDate(date, 'Y-m-d H:i:s').format(displayFormat);
		}
		return '-';
	},

	/**
	 * Used in metaPanel
	 * @param {Object} v
	 */
	metaPanelDateRenderer: function(v){
		if (v && v.substr(0, 4) == '0000') {
			return '' + __('Invalid date') + '';
		}
		return v ? Garp.renderers.intelliDateTimeRenderer(v) : '' + __('No date specified') + '';
	},

	/**
	 * Date only
	 * @param {Object} date
	 */
	dateRenderer: function(date, meta, rec){
		var displayFormat = 'd F Y';
		if (Ext.isDate(date)) {
			var v =  date.format(displayFormat);
			return v;
		}
		if (date && typeof Date.parseDate(date, 'Y-m-d') != 'undefined') {
			return Date.parseDate(date, 'Y-m-d').format(displayFormat);
		} else if (date && typeof Date.parseDate(date, 'd F Y') != 'undefined') {
			return Date.parseDate(date, 'd F Y').format(displayFormat);
		}
		return '-';
	},

	/**
	 *
	 * @param {Date/String} date
	 * @param {Object} meta
	 * @param {Object} rec
	 */
	timeRenderer: function(date, meta, rec){
		var displayFormat = 'H:i';
		if (Ext.isDate(date)) {
			var v =  date.format(displayFormat);
			return v;
		}
		if (date && typeof Date.parseDate(date, 'H:i:s') != 'undefined') {
			return Date.parseDate(date, 'H:i:s').format(displayFormat);
		} else if (date && typeof Date.parseDate(date, 'H:i') != 'undefined') {
			return Date.parseDate(date, 'H:i').format(displayFormat);
		}
		return '-';
	},

	/**
	 * Year
	 * @param {Date/String} date
	 */
	yearRenderer: function(date, meta, rec){
		var displayFormat = 'Y';
		if (Ext.isDate(date)) {
			return date.format(displayFormat);
		}
		if (date && typeof Date.parseDate(date,'Y') != 'undefined') {
			return Date.parseDate(date, 'Y').format(displayFormat);
		}
		return '-';
	},

	/**
	 * For use in Forms.
	 *
	 * Displays today @ time, yesterday @ time or just the date (WITHOUT time)
	 *
	 * @TODO Decide if this also needs to go into grids. Make adjustments then.
	 * @param {Date} date
	 */
	intelliDateTimeRenderer: function(date){
		var now = new Date();
		var yesterday = new Date().add(Date.DAY, -1);
		date = Date.parseDate(date, 'Y-m-d H:i:s');
		if (!date) {
			return;
		}
		if(date.getYear() == now.getYear()){
			if(date.getMonth() == now.getMonth()){
				if(date.getDate() == yesterday.getDate()){
					return __('Yesterday at') + ' ' + date.format('H:i');
				}
				if(date.getDate() == now.getDate()){
					//if(date.getMinutes() == now.getMinutes()){
					//	return __('a few seconds ago');
					//}
					return __('Today at') + ' ' + date.format('H:i');
				}
				return date.format('j M');
			}
		}
		return date.format('j M Y');
	},

	/**
	 * Image
	 * @param {Object} val
	 */
	imageRenderer: function(val, meta, record, options){
		if(!record){
			record = {
				id: 0
			};
		}
		if(!Ext.isObject(options)){
			options = {
				size: 64
			};
		}
		var localTpl = new Ext.Template('

', {
			compile: false,
			disableFormats: true
		});
		var remoteTpl = new Ext.Template('

', {
			compile: false,
			disableFormats: true
		});

		if (typeof record == 'undefined') {
			return __('New Image');
		}

		var v = val ? (/^https?:\/\//.test(val) ? remoteTpl.apply([val]) : localTpl.apply([val])) : __('No Image uploaded');
		return v;
	},

	/**
	 *
	 * @param {String} val image Id
	 */
	imageRelationRenderer: function(val){
		var imgHtml = val ? '' : '

';
		return '
'+imgHtml+'
';
	},

	/**
	 * Image rendererer primarily intended for Garp.formPanel, not realy appropriate as a column renderer. Use imageRenderer instead
	 * @param {String} val
	 */
	imagePreviewRenderer: function(val,meta,record){
		var tpl = new Ext.Template('

', {
			compile: true,
			disableFormats: true
		});
		if(typeof record == 'undefined'){
			record = {
				phantom : true
			};
		}
		var v =  val ? tpl.apply([val]) : record.phantom === true ? __('New Image') : __('No Image uploaded');
		return v;
	},

	/**
	 *
	 * @param {String} val
	 */
	uploadedImagePreviewRenderer: function(val){
		var tpl = new Ext.Template('

', {
			compile: true,
			disableFormats: true
		});
		return tpl.apply(val);
	},

	/**
	 * cropPreviewRenderer
	 */
	cropPreviewRenderer: function(val, meta, record){
		if (record.get('w') && record.get('h')) {
			var size = 32;
			var w = record.get('w') / record.get('h') * size;
			var h = record.get('h') / record.get('w') * size;
			if (w > size) {
				h = h * (size / w);
				w = size;
			}
			if (h > size) {
				w = w * (size / h);
				h = size;
			}
			if (h == w) {
				w = h = size * 0.75;
			}
			var mt = (size - h) / 2;
			var ml = (size - w) / 2;
			h = Math.ceil(h);
			w = Math.ceil(w);
			mt = Math.floor(mt);
			ml = Math.floor(ml);

			return '

';
		} else {
			return '';
		}
	},

	/**
	 *
	 * @param {Number} row
	 * @param {Number} cell
	 * @param {Object} view
	 */
	checkVisible: function(row, cell, view){
		if(view.getRow(row)){
			return Ext.get(view.getCell(row, cell)).isVisible(true);
		}
		return false;
	},

	/**
	 * remoteDisplayFieldRenderer
	 * grabs the external model and uses its displayField
	 *
	 * Usage from extended model:
	 * @example this.addColumn({
	 *  // [...]
	 *  sortable: false
	 * 	dataIndex: 'Cinema'
	 * 	renderer: Garp.renderers.remoteDisplayFieldRenderer.createDelegate(null, ['Cinema'], true) // no scope, Cinema model, append arguments
	 * });
	 *
	 * @param {Object} val
	 * @param {Object} meta
	 * @param {Object} rec
	 * @param {Number} rI
	 * @param {Number} cI
	 * @param {Object} store
	 * @param {Object} view
	 * @param {String} modelName
	 */
	remoteDisplayFieldRenderer: function(val, meta, rec, rI, cI, store, view, modelName){
		if (!modelName || !val) {
			return;
		}
		view.on('refresh', function(){
				if (Garp.renderers.checkVisible(rI, cI, view)) {
					Garp[modelName].fetch({
						query: {
							id: val
						}
					}, function(res){
						// make res an ultra ligtweight 'pseudo record':
						if (res.rows && res.rows[0]) {
							res.rows[0].get = function(v){
								return this[v] || '';
							};
							var text = Garp.dataTypes[modelName].displayFieldRenderer(res.rows[0]);
							if (Ext.get(view.getCell(rI, cI))) {
								Ext.get(view.getCell(rI, cI)).update('
' + text + '
');
							}
						}
					});
				}

		}, {
			buffer: 200,
			scope: this
		});

		return '

';
	},

	checkboxRenderer: function(v){
		return v == '1'  ? __('yes') : __('no');
	}
});
<commit_msg>Image relation renderer<commit_after>Ext.ns('Garp.renderers');

/**
 * General Column Renderers & Field converters
 *
 */
Ext.apply(Garp.renderers,{

	/**
	 * Converter for fullname fields. Combines first, prefix, last into one field. Usefull e.g. as displayField for relationFields
	 * @param {Object} v field value (not used)
	 * @param {Object} rec Ext.Record
	 */
	fullNameConverter: function(v, rec){
		var name = [];
		Ext.each(['first_name', 'last_name_prefix', 'last_name'], function(f){
			if(rec[f]){
				name.push(rec[f]);
			}
		});
		if(!name){
			name = rec.name;
		}
		return name.join(' ');
	},

	/**
	 * Google address resolver. Forms only
	 * @param {Object} arr
	 * @return {String} formated address
	 */
	geocodeAddressRenderer: function(arr){
		arr = arr.address_components;
		var country_long,
		country,
		city,
		street,
		housenumber,
		out = '';

		Ext.each(arr, function(i){
			if(i.types.indexOf('country') > -1){
				country_long = i.long_name;
				country = i.short_name;
			}
			if(i.types.indexOf('locality') > -1){
				city = i.short_name;
			}
			if(i.types.indexOf('route') > -1){
				street = i.short_name;
			}
			if(i.types.indexOf('street_number') > -1){
				housenumber = i.short_name;
			}

		});
		if (housenumber) {
			out = street + ' ' + housenumber;
		} else if (street) {
			out = street;
		}
		if(city){
			out += ', ' + city;
		}
		if(country != 'NL'){
			out += ', ' + country_long;
		}
		return out;
	},

	/**
	 * We do not want HTML to be viewed in a grid column. This causes significant performance hogs and Adobe Flash™ bugs otherwise...
	 */
	htmlRenderer: function(){
		return '';
	},

	/**
	 * Shorter Date & Time
	 * @param {Date/String} date
	 */
	shortDateTimeRenderer: function(date, meta, rec){
		var displayFormat = 'd M Y H:i';
		if (Ext.isDate(date)) {
			return date.format(displayFormat);
		}
		if (date && typeof Date.parseDate(date,'Y-m-d H:i:s') != 'undefined') {
			return Date.parseDate(date, 'Y-m-d H:i:s').format(displayFormat);
		}
		return '-';
	},

	/**
	 * Date & Time
	 * @param {Date/String} date
	 */
	dateTimeRenderer: function(date, meta, rec){
		var displayFormat = 'd F Y H:i';
		if (Ext.isDate(date)) {
			return date.format(displayFormat);
		}
		if (date && typeof Date.parseDate(date,'Y-m-d H:i:s') != 'undefined') {
			return Date.parseDate(date, 'Y-m-d H:i:s').format(displayFormat);
		}
		return '-';
	},

	/**
	 * Used in metaPanel
	 * @param {Object} v
	 */
	metaPanelDateRenderer: function(v){
		if (v && v.substr(0, 4) == '0000') {
			return '' + __('Invalid date') + '';
		}
		return v ? Garp.renderers.intelliDateTimeRenderer(v) : '' + __('No date specified') + '';
	},

	/**
	 * Date only
	 * @param {Object} date
	 */
	dateRenderer: function(date, meta, rec){
		var displayFormat = 'd F Y';
		if (Ext.isDate(date)) {
			var v =  date.format(displayFormat);
			return v;
		}
		if (date && typeof Date.parseDate(date, 'Y-m-d') != 'undefined') {
			return Date.parseDate(date, 'Y-m-d').format(displayFormat);
		} else if (date && typeof Date.parseDate(date, 'd F Y') != 'undefined') {
			return Date.parseDate(date, 'd F Y').format(displayFormat);
		}
		return '-';
	},

	/**
	 *
	 * @param {Date/String} date
	 * @param {Object} meta
	 * @param {Object} rec
	 */
	timeRenderer: function(date, meta, rec){
		var displayFormat = 'H:i';
		if (Ext.isDate(date)) {
			var v =  date.format(displayFormat);
			return v;
		}
		if (date && typeof Date.parseDate(date, 'H:i:s') != 'undefined') {
			return Date.parseDate(date, 'H:i:s').format(displayFormat);
		} else if (date && typeof Date.parseDate(date, 'H:i') != 'undefined') {
			return Date.parseDate(date, 'H:i').format(displayFormat);
		}
		return '-';
	},

	/**
	 * Year
	 * @param {Date/String} date
	 */
	yearRenderer: function(date, meta, rec){
		var displayFormat = 'Y';
		if (Ext.isDate(date)) {
			return date.format(displayFormat);
		}
		if (date && typeof Date.parseDate(date,'Y') != 'undefined') {
			return Date.parseDate(date, 'Y').format(displayFormat);
		}
		return '-';
	},

	/**
	 * For use in Forms.
	 *
	 * Displays today @ time, yesterday @ time or just the date (WITHOUT time)
	 *
	 * @TODO Decide if this also needs to go into grids. Make adjustments then.
	 * @param {Date} date
	 */
	intelliDateTimeRenderer: function(date){
		var now = new Date();
		var yesterday = new Date().add(Date.DAY, -1);
		date = Date.parseDate(date, 'Y-m-d H:i:s');
		if (!date) {
			return;
		}
		if(date.getYear() == now.getYear()){
			if(date.getMonth() == now.getMonth()){
				if(date.getDate() == yesterday.getDate()){
					return __('Yesterday at') + ' ' + date.format('H:i');
				}
				if(date.getDate() == now.getDate()){
					//if(date.getMinutes() == now.getMinutes()){
					//	return __('a few seconds ago');
					//}
					return __('Today at') + ' ' + date.format('H:i');
				}
				return date.format('j M');
			}
		}
		return date.format('j M Y');
	},

	/**
	 * Image
	 * @param {Object} val
	 */
	imageRenderer: function(val, meta, record, options){
		if(!record){
			record = {
				id: 0
			};
		}
		if(!Ext.isObject(options)){
			options = {
				size: 64
			};
		}
		var localTpl = new Ext.Template('

', {
			compile: false,
			disableFormats: true
		});
		var remoteTpl = new Ext.Template('

', {
			compile: false,
			disableFormats: true
		});

		if (typeof record == 'undefined') {
			return __('New Image');
		}

		var v = val ? (/^https?:\/\//.test(val) ? remoteTpl.apply([val]) : localTpl.apply([val])) : __('No Image uploaded');
		return v;
	},

	/**
	 * Relation Renderer for buttons & columnModel
	 * @param {String} val image Id
	 * @param {Object} meta information. Only used in columnModel.renderer setup
	 */
	imageRelationRenderer: function(val, meta, record){
		var imgHtml = '';
		if (meta) {	// column model renderer
			if (!val) {
				imgHtml = '

';
			}
			return '
' + imgHtml + '
';
		} else {	// button
			return val ? imgHtml : null;
		}
	},

	/**
	 * Image rendererer primarily intended for Garp.formPanel, not realy appropriate as a column renderer. Use imageRenderer instead
	 * @param {String} val
	 */
	imagePreviewRenderer: function(val,meta,record){
		var tpl = new Ext.Template('

', {
			compile: true,
			disableFormats: true
		});
		if(typeof record == 'undefined'){
			record = {
				phantom : true
			};
		}
		var v =  val ? tpl.apply([val]) : record.phantom === true ? __('New Image') : __('No Image uploaded');
		return v;
	},

	/**
	 *
	 * @param {String} val
	 */
	uploadedImagePreviewRenderer: function(val){
		var tpl = new Ext.Template('

', {
			compile: true,
			disableFormats: true
		});
		return tpl.apply(val);
	},

	/**
	 * cropPreviewRenderer
	 */
	cropPreviewRenderer: function(val, meta, record){
		if (record.get('w') && record.get('h')) {
			var size = 32;
			var w = record.get('w') / record.get('h') * size;
			var h = record.get('h') / record.get('w') * size;
			if (w > size) {
				h = h * (size / w);
				w = size;
			}
			if (h > size) {
				w = w * (size / h);
				h = size;
			}
			if (h == w) {
				w = h = size * 0.75;
			}
			var mt = (size - h) / 2;
			var ml = (size - w) / 2;
			h = Math.ceil(h);
			w = Math.ceil(w);
			mt = Math.floor(mt);
			ml = Math.floor(ml);

			return '

';
		} else {
			return '';
		}
	},

	/**
	 *
	 * @param {Number} row
	 * @param {Number} cell
	 * @param {Object} view
	 */
	checkVisible: function(row, cell, view){
		if(view.getRow(row)){
			return Ext.get(view.getCell(row, cell)).isVisible(true);
		}
		return false;
	},

	/**
	 * remoteDisplayFieldRenderer
	 * grabs the external model and uses its displayField
	 *
	 * Usage from extended model:
	 * @example this.addColumn({
	 *  // [...]
	 *  sortable: false
	 * 	dataIndex: 'Cinema'
	 * 	renderer: Garp.renderers.remoteDisplayFieldRenderer.createDelegate(null, ['Cinema'], true) // no scope, Cinema model, append arguments
	 * });
	 *
	 * @param {Object} val
	 * @param {Object} meta
	 * @param {Object} rec
	 * @param {Number} rI
	 * @param {Number} cI
	 * @param {Object} store
	 * @param {Object} view
	 * @param {String} modelName
	 */
	remoteDisplayFieldRenderer: function(val, meta, rec, rI, cI, store, view, modelName){
		if (!modelName || !val) {
			return;
		}
		view.on('refresh', function(){
				if (Garp.renderers.checkVisible(rI, cI, view)) {
					Garp[modelName].fetch({
						query: {
							id: val
						}
					}, function(res){
						// make res an ultra ligtweight 'pseudo record':
						if (res.rows && res.rows[0]) {
							res.rows[0].get = function(v){
								return this[v] || '';
							};
							var text = Garp.dataTypes[modelName].displayFieldRenderer(res.rows[0]);
							if (Ext.get(view.getCell(rI, cI))) {
								Ext.get(view.getCell(rI, cI)).update('
' + text + '
');
							}
						}
					});
				}

		}, {
			buffer: 200,
			scope: this
		});

		return '

';
	},

	checkboxRenderer: function(v){
		return v == '1'  ? __('yes') : __('no');
	}
});
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import MqttClientSingleton from '../util/MqttClientSingleton'
import JSONClientSingleton from '../util/JSONClientSingleton'
import './SwitchOnOff.css';

class SwitchOnOff extends Component {

  constructor(props) {
    super(props);
    if (this.props.isScene) {
      this.json = new JSONClientSingleton();
    } else {
      this.mqtt = new MqttClientSingleton();
    }
  }

  getPushCommand = () => {
    if (this.props.pushOn) {
      return 'On'
    } else if (this.props.pushOff) {
      return 'Off'
    }
    const isOn = this.props.isScene ? this.props.valueText === 'On' :
        this.props.value === 1;
    return isOn ? 'Off' : 'On';
  }

  handleClick = (event) => {
    if (this.props.readOnly) {
      return
    }
    const command = this.getPushCommand();
    if (this.props.isScene) {
      const message = {
        type: 'command',
        param: 'switchscene',
        idx: this.props.idx,
        switchcmd: command
      };
      this.json.get(message, this.props.onChange);
    } else {
      const message = {
        command: 'switchlight',
        idx: this.props.idx,
        switchcmd: command,
        level: 100
      };
      this.mqtt.publish(message);
    }
  }

  getButtonStyle() {
    const theme = this.props.theme;
    if (!theme) {
      return {};
    }
    const style = {
      color: theme.textOn,
      background: this.props.readOnly ? theme.unlockedBackground : theme.buttonOn
    }
    const gradient = 'repeating-linear-gradient(-45deg, _corner, _corner 20%, _bg 20%, _bg 80%, _corner 80%, _corner 100%)';
    if (this.props.pushOn && this.props.isScene) {
      style.color = theme.textMixed;
      style.background = this.props.readOnly ? theme.unlockedBackground : gradient.replace(/_corner/g, theme.buttonMixed).replace(/_bg/g, theme.buttonOff);
      return style;
    }
    if (this.props.valueText === "Mixed") {
      style.color = theme.textMixed;
      style.background = this.props.readOnly ? theme.unlockedBackground : gradient.replace(/_corner/g, theme.buttonOn).replace(/_bg/g, theme.buttonOff);
    }
    if (this.props.valueText === "Off" || this.props.value === 0) {
      style.color = theme.textOff;
      style.background =  this.props.readOnly ? theme.unlockedBackground : theme.buttonOff;
    }
    return style;
  }

  render() {
    const valueText = this.props.valueText || (this.props.value === 0 ? 'Off' : 'On');
    return (
      <button className="switch" style={this.getButtonStyle()} onClick={this.handleClick} title={valueText}>{this.props.label}</button>
    );
  }

}

export default SwitchOnOff
<commit_msg>Change Group button behavior when clicking on it in Mixed state, from Mixed->All On to Mixed->All Off. I've been thinking for a long time that it would be preferable, and some user feedback on the forums confirmed it.<commit_after>import React, { Component } from 'react';
import MqttClientSingleton from '../util/MqttClientSingleton'
import JSONClientSingleton from '../util/JSONClientSingleton'
import './SwitchOnOff.css';

class SwitchOnOff extends Component {

  constructor(props) {
    super(props);
    if (this.props.isScene) {
      this.json = new JSONClientSingleton();
    } else {
      this.mqtt = new MqttClientSingleton();
    }
  }

  getPushCommand = () => {
    if (this.props.pushOn) {
      return 'On'
    } else if (this.props.pushOff) {
      return 'Off'
    }
    const isOn = this.props.isScene ? this.props.valueText !== 'Off' :
        this.props.value === 1;
    return isOn ? 'Off' : 'On';
  }

  handleClick = (event) => {
    if (this.props.readOnly) {
      return
    }
    const command = this.getPushCommand();
    if (this.props.isScene) {
      const message = {
        type: 'command',
        param: 'switchscene',
        idx: this.props.idx,
        switchcmd: command
      };
      this.json.get(message, this.props.onChange);
    } else {
      const message = {
        command: 'switchlight',
        idx: this.props.idx,
        switchcmd: command,
        level: 100
      };
      this.mqtt.publish(message);
    }
  }

  getButtonStyle() {
    const theme = this.props.theme;
    if (!theme) {
      return {};
    }
    const style = {
      color: theme.textOn,
      background: this.props.readOnly ? theme.unlockedBackground : theme.buttonOn
    }
    const gradient = 'repeating-linear-gradient(-45deg, _corner, _corner 20%, _bg 20%, _bg 80%, _corner 80%, _corner 100%)';
    if (this.props.pushOn && this.props.isScene) {
      style.color = theme.textMixed;
      style.background = this.props.readOnly ? theme.unlockedBackground : gradient.replace(/_corner/g, theme.buttonMixed).replace(/_bg/g, theme.buttonOff);
      return style;
    }
    if (this.props.valueText === "Mixed") {
      style.color = theme.textMixed;
      style.background = this.props.readOnly ? theme.unlockedBackground : gradient.replace(/_corner/g, theme.buttonOn).replace(/_bg/g, theme.buttonOff);
    }
    if (this.props.valueText === "Off" || this.props.value === 0) {
      style.color = theme.textOff;
      style.background =  this.props.readOnly ? theme.unlockedBackground : theme.buttonOff;
    }
    return style;
  }

  render() {
    const valueText = this.props.valueText || (this.props.value === 0 ? 'Off' : 'On');
    return (
      <button className="switch" style={this.getButtonStyle()} onClick={this.handleClick} title={valueText}>{this.props.label}</button>
    );
  }

}

export default SwitchOnOff
<|endoftext|>


<commit_before>timeStart = Date.now();

$('.rep').on('click',function(e){
	openAndPlay(e.target.innerHTML);
});

$('#findExo1').on('click',function(e){
	openAndPlay($('#find').attr("note"));
});

$(
	function() {
	var e = random_solution(notes,1);
	$('#find').attr("note", e[0].label);
	changeColorButton(notes);
});

$('#sendAnswers').on('click',function(e){
	var time = Date.now() - timeStart;
	// calcul du nombre d'erreur

	var response = $('#findExo1').attr('note');
	var answerUser = $('input[name=answer]:checked').attr('note');

	var nbErr = 0;
	if(response !== answerUser){
		nbErr = 1;
	}

	if(typeof(answerUser) === 'undefined'){
		var content = '
Tu as oublié de donner une réponse !
';
		$('#message').html(content);
	} else {
		//requete ajax
		sendAnswerToFuzzy(nbErr, $('#nbResponses').val(), time);
		getNextExercices($('#idExercice').val());
		getFuzzyNote(12);
	}
});
<commit_msg>correction exercice1 pour le son find = findExo1<commit_after>timeStart = Date.now();

$('.rep').on('click',function(e){
	openAndPlay(e.target.innerHTML);
});

$('#findExo1').on('click',function(e){
	openAndPlay($('#findExo1').attr("note"));
});

$(
	function() {
	var e = random_solution(notes,1);
	$('#findExo1').attr("note", e[0].label);
	changeColorButton(notes);
});

$('#sendAnswers').on('click',function(e){
	var time = Date.now() - timeStart;
	// calcul du nombre d'erreur

	var response = $('#findExo1').attr('note');
	var answerUser = $('input[name=answer]:checked').attr('note');

	var nbErr = 0;
	if(response !== answerUser){
		nbErr = 1;
	}

	if(typeof(answerUser) === 'undefined'){
		var content = '
Tu as oublié de donner une réponse !
';
		$('#message').html(content);
	} else {
		//requete ajax
		sendAnswerToFuzzy(nbErr, $('#nbResponses').val(), time);
		getNextExercices($('#idExercice').val());
		getFuzzyNote(12);
	}
});
<|endoftext|>


<commit_before>var _ = require('underscore');
var Backbone = require('backbone');
var WindshaftFiltersCollection = require('./filters/collection');
var WindshaftFiltersBoundingBoxFilter = require('./filters/bounding_box');
var WindshaftDashboardInstance = require('./dashboard-instance');

var WindshaftDashboard = function(options) {
  BOUNDING_BOX_FILTER_WAIT = 500;

  this.layerGroup = options.layerGroup;
  this.layers = new Backbone.Collection(options.layers);
  this.widgets = options.widgets;
  this.map = options.map;
  this.client = options.client;
  this.statTag = options.statTag;
  this.configGenerator = options.configGenerator;

  this.instance = new WindshaftDashboardInstance();

  this.map.bind('change:center change:zoom', _.debounce(this._boundingBoxChanged, BOUNDING_BOX_FILTER_WAIT), this);

  this.layers.bind('change', this._layerChanged, this);
  this.widgets.bind('change:filter', this._filterChanged, this);

  this._createInstance();
};

WindshaftDashboard.prototype._createInstance = function(options) {
  var options = options || {};

  var dashboardConfig = this.configGenerator.generate({
    layers: this.layers.models,
    widgets: this.widgets
  });

  var filtersFromVisibleLayers = this.widgets.chain()
    .filter(function(w) { return w.layer.isVisible() })
    .map(function(w) { return w.filter })
    .compact() // not all widgets have filters
    .value();

  var filters = new WindshaftFiltersCollection(filtersFromVisibleLayers);

  this.client.instantiateMap({
    mapDefinition: dashboardConfig,
    filters: filters.toJSON(),
    success: function(dashboardInstance) {

      // Update the dashboard instance with the attributes of the new one
      this.instance.set(dashboardInstance.toJSON());

      // TODO: Set the URL of the attributes service once it's available
      this.layerGroup && this.layerGroup.set({
        baseURL: dashboardInstance.getBaseURL(),
        urls: dashboardInstance.getTiles('mapnik')
      });

      // update other kind of layers too
      this.layers.each(function(layer, layerIndex) {
        layer.set('meta', dashboardInstance.getLayerMeta(layerIndex));
        if (layer.get('type') === 'torque') {
          layer.set('urls', dashboardInstance.getTiles('torque'));
        }
      });

      this._updateWidgetURLs({
        layerId: options.layerId
      });

    }.bind(this),
    error: function(error) {
      console.log('Error creating dashboard instance: ' + error);
    }
  });

  return this.instance;
};

WindshaftDashboard.prototype._boundingBoxChanged = function() {
  if (this.instance.isLoaded()) {
    this._updateWidgetURLs();
  }
};

WindshaftDashboard.prototype._updateWidgetURLs = function(options) {
  options = options || {};
  var boundingBoxFilter = new WindshaftFiltersBoundingBoxFilter(this.map.getViewBounds());
  var boundingBox = boundingBoxFilter.toString();
  var layerId = options.layerId;

  this.widgets.each(function(widget) {
    var url = this.instance.getWidgetURL({
      widgetId: widget.get('id'),
      protocol: 'http'
    });

    widget.set({
      'url': url,
      'boundingBox': boundingBox
    }, {
      silent: layerId && layerId !== widget.layer.get('id')
    });
  }, this);
};

WindshaftDashboard.prototype._filterChanged = function(w) {
  this._createInstance({
    layerId: w.layer.get('id')
  });
};

WindshaftDashboard.prototype._layerChanged = function(layer) {
  var layerId = layer.get('id');
  this._createInstance({
    layerId: layerId
  });
};

module.exports = WindshaftDashboard;
<commit_msg>Set extra attributes to the widget model when found in layer's meta<commit_after>var _ = require('underscore');
var Backbone = require('backbone');
var WindshaftFiltersCollection = require('./filters/collection');
var WindshaftFiltersBoundingBoxFilter = require('./filters/bounding_box');
var WindshaftDashboardInstance = require('./dashboard-instance');

var WindshaftDashboard = function(options) {
  BOUNDING_BOX_FILTER_WAIT = 500;

  this.layerGroup = options.layerGroup;
  this.layers = new Backbone.Collection(options.layers);
  this.widgets = options.widgets;
  this.map = options.map;
  this.client = options.client;
  this.statTag = options.statTag;
  this.configGenerator = options.configGenerator;

  this.instance = new WindshaftDashboardInstance();

  this.map.bind('change:center change:zoom', _.debounce(this._boundingBoxChanged, BOUNDING_BOX_FILTER_WAIT), this);

  this.layers.bind('change', this._layerChanged, this);
  this.widgets.bind('change:filter', this._filterChanged, this);

  this._createInstance();
};

WindshaftDashboard.prototype._createInstance = function(options) {
  var options = options || {};

  var dashboardConfig = this.configGenerator.generate({
    layers: this.layers.models,
    widgets: this.widgets
  });

  var filtersFromVisibleLayers = this.widgets.chain()
    .filter(function(w) { return w.layer.isVisible() })
    .map(function(w) { return w.filter })
    .compact() // not all widgets have filters
    .value();

  var filters = new WindshaftFiltersCollection(filtersFromVisibleLayers);

  this.client.instantiateMap({
    mapDefinition: dashboardConfig,
    filters: filters.toJSON(),
    success: function(dashboardInstance) {

      // Update the dashboard instance with the attributes of the new one
      this.instance.set(dashboardInstance.toJSON());

      // TODO: Set the URL of the attributes service once it's available
      this.layerGroup && this.layerGroup.set({
        baseURL: dashboardInstance.getBaseURL(),
        urls: dashboardInstance.getTiles('mapnik')
      });

      // update other kind of layers too
      this.layers.each(function(layer, layerIndex) {
        layer.set('meta', dashboardInstance.getLayerMeta(layerIndex));
        if (layer.get('type') === 'torque') {
          layer.set('urls', dashboardInstance.getTiles('torque'));
        }
      });

      this._updateWidgetURLs({
        layerId: options.layerId
      });

    }.bind(this),
    error: function(error) {
      console.log('Error creating dashboard instance: ' + error);
    }
  });

  return this.instance;
};

WindshaftDashboard.prototype._boundingBoxChanged = function() {
  if (this.instance.isLoaded()) {
    this._updateWidgetURLs();
  }
};

WindshaftDashboard.prototype._updateWidgetURLs = function(options) {
  options = options || {};
  var boundingBoxFilter = new WindshaftFiltersBoundingBoxFilter(this.map.getViewBounds());
  var boundingBox = boundingBoxFilter.toString();
  var layerId = options.layerId;

  this.widgets.each(function(widget) {
    var url = this.instance.getWidgetURL({
      widgetId: widget.get('id'),
      protocol: 'http'
    });

    var layerMeta = widget.layer.get('meta') || {};
    var extraAttrs = {};
    if (layerMeta.steps && layerMeta.column_type && _.isNumber(layerMeta.start) && _.isNumber(layerMeta.end)) {
      extraAttrs = {
        bins: layerMeta.steps,
        columnType: layerMeta.column_type,
        start: layerMeta.start  / 1000,
        end:  layerMeta.end / 1000
      };
    }

    widget.set(_.extend({
      'url': url,
      'boundingBox': boundingBox
    }, extraAttrs), {
      silent: layerId && layerId !== widget.layer.get('id')
    });
  }, this);
};

WindshaftDashboard.prototype._filterChanged = function(w) {
  this._createInstance({
    layerId: w.layer.get('id')
  });
};

WindshaftDashboard.prototype._layerChanged = function(layer) {
  var layerId = layer.get('id');
  this._createInstance({
    layerId: layerId
  });
};

module.exports = WindshaftDashboard;
<|endoftext|>