Iterative innovation VS disruptive innovation

When we talk about innovation, we mostly talk about new technologies like Google or Facebook. But what we should remember is that innovation is everywhere. Innovation could be divided in two categories : iterative innovation or disruptive innovation.

Iterative innovation

Start with something you know and go beyond.

Iterative innovation is the ability to improve what's currently available. We have a lot of examples to demonstrate what is iterative innovation. For example, in the food industry (restaurant), fast-foods are an alternative to existing restaurants but they are based on the same principles : deliver foods in a place.

There is also a various number of differentiation factors. The most common factors are time (delay), cost and quality. Indeed, a fast-food is generally cheaper and faster to deliver food than a traditional restaurant.

Of course, iterative innovation is more common than disruptive innovation.

Disruptive innovation

Start from scratch and break the rules.

Disruptive innovation appears when you completely change habits. Again, if we use the food industry example, we can try to find a way that people has not experimented yet, a way that people could have interest. Disruptive innovation in food industry could be food injection.

When we are working on innovation, customers are more willing to accept the change if it is an iterative innovation because they already know something similar before.

Shu-Ha-Ri and innovation

We can try to find a link between Shu-Ha-Ri and innovation. Here is the three steps :

  • Shu - we start to learn so we are not going to make innovation at this time, we try to reproduce something that already exist without any value added
  • Ha - we have now a strong knowledge and we can make some improvements based on what we know, so this could be considered as iterative innovation
  • Ri - we are now an expert (maybe more) and we are able to go beyond what we learned, this is where disruptive innovation could happen
Discovering WinJS : Episode 6 - Realtime messages

Discovering WinJS : Episode 6 - Realtime messages

We are able to show current room messages. But what about realtime messages sent when you're using the app ? We'll how to use realtime Gitter API thanks to Faye framework.

A Faye client

First of all, you need Faye :

npm install --save faye  

With that package, you can add a reference in your HTML page :

<script src="node_modules/faye/browser/faye-browser-min.js"></script>  

Be careful ! Using faye in WinJS could not work. Indeed, if you use it, you could have an error message that prevent use of unsafe code. To allow faye to execute in your app, you need to update your appxmanifest :

<Application Id="App" StartPage="ms-appx-web:///default.html">  
<uap:ApplicationContentUriRules>  
  <uap:Rule Type="include" Match="ms-appx-web:///" WindowsRuntimeAccess="all"/>
</uap:ApplicationContentUriRules>  

Create a Realtime service

Great, now we can create our Faye client and subscribe to realtime events. It as simple as this line of code :

var client = new Faye.Client('https://ws.gitter.im/faye', { timeout: 60, retry: 5, interval: 1 });  

As you know, Gitter API requires an access token. To allow Faye client to access the server, you need to set token through an extension :

var ClientAuthExt = function () { };

ClientAuthExt.prototype.outgoing = function (message, callback) {  
    if (message.channel == '/meta/handshake') {
        if (!message.ext) {
            message.ext = {};
        }
        message.ext.token = OAuthService.refreshToken;
    }

    callback(message);
};

ClientAuthExt.prototype.incoming = function (message, callback) {  
    if (message.channel == '/meta/handshake') {
        if (message.successful) {
            console.log('Successfuly subscribed');
        } else {
            console.log('Something went wrong: ', message.error);
        }
    }

    callback(message);
};

client.addExtension(new ClientAuthExt());  

Finally, you have to subscribe to a faye endpoint (event). Here is the list of existing Gitter endpoints : https://developer.gitter.im/docs/faye-endpoint

client.subscribe('/api/v1/rooms/' + roomId + '/chatMessages', function (response) {  
    var message = response.model;
    callback(roomId, message);
});

And because we love Angular, here is the result of a RealtimeService :

.service('RealtimeApiService', function (ConfigService, OAuthService) {
    var realtimeApiService = this;

    realtimeApiService.initialize = function () {
        return new Promise((done, error) => {
            var ClientAuthExt = function () { };

            ClientAuthExt.prototype.outgoing = function (message, callback) {
                if (message.channel == '/meta/handshake') {
                    if (!message.ext) {
                        message.ext = {};
                    }
                    message.ext.token = OAuthService.refreshToken;
                }

                callback(message);
            };

            ClientAuthExt.prototype.incoming = function (message, callback) {
                if (message.channel == '/meta/handshake') {
                    if (message.successful) {
                        console.log('Successfuly subscribed');
                    } else {
                        console.log('Something went wrong: ', message.error);
                    }
                }

                callback(message);
            };

            realtimeApiService.client = new Faye.Client('https://ws.gitter.im/faye', { timeout: 60, retry: 5, interval: 1 });
            realtimeApiService.client.addExtension(new ClientAuthExt());

            done();
        });
    };

    realtimeApiService.subscribe = function (roomId, callback) {
        // subscribe to realtime messages
        realtimeApiService.client.subscribe('/api/v1/rooms/' + roomId + '/chatMessages', function (response) {
            var message = response.model;
            callback(roomId, message);
        });
    };

    return realtimeApiService;
})

Push new messages in listview

Now, using this new service, we can handle the reception of new messages :

// subscribe to realtime messages
RealtimeApiService.subscribe($scope.rooms[i].id, function (roomId, message) {  
    if ($scope.currentRoom && $scope.currentRoom.id === roomId) {
        $scope.list.push(message);
    }

    // TODO : send notification
});

You may ask why I use $scope.list, and where it comes from. When I select a room, I set a list of messages. This list is a WinJS.Binding.List, if you update it, it will update the binded ListView.

// refresh ListView of messages
$scope.list = new WinJS.Binding.List($scope.messages);
messagesListView.itemDataSource = $scope.list.dataSource;  

And here we are, each time a new message is sent, you can see it in your ListView.

Discovering WinJS : Episode 5 - The ListView

Discovering WinJS : Episode 5 - The ListView

Now we have access to every room, we can get info of a single room. Of course, a chat room contains messages, a list of messages. Hopefully, WinJS has a UI control to handle lists : the ListView.

Retrieve messages

First of all, you need to retrieve messages. To do that, you have to create a new method to retrieve messages based on id of selected room. In this example, we get the last 50 messages of the room.

apiService.getMessages = function (roomId) {  
    return new Promise((done, error) => {
        WinJS.xhr({
            type: 'GET',
            url: ConfigService.baseUrl + "rooms/" + roomId + "/chatMessages?limit=50",
            headers: {
                "Accept": "application/json",
                "Content-Type": "application/json",
                "Authorization": "Bearer " + OAuthService.refreshToken
            }
        })
            .then(function (success) {
                done(JSON.parse(success.response));
            });
    });
};

Show messages

From the sample provided on the website (http://winjs.azurewebsites.net/#listview), we will create a list to show messages.

An item template

You first need a data template that represent how your item (in list) is showed. As gitter API send HTML content for each message, we bind our template content to that.

<div class="messageTemplate" data-win-control="WinJS.Binding.Template" style="display: none;">  
    <div class="smallListIconTextItem">
        <div data-win-bind="innerHTML: html"></div>
    </div>
</div>  

The listview control

And then, the ListView. We oviously use our previous data template (as an itemTemplate).

<div id="messagesListView"  
        data-win-control="WinJS.UI.ListView"
        data-win-options="{
        itemTemplate: select('.messageTemplate'),
        selectionMode: 'none',
        tapBehavior: 'none',
        layout: { type: WinJS.UI.ListLayout }
        }">
</div>  

Be careful ! You may have noticed that, by default, every list-view item has the same height. So, it's a bit ugly because not all your messages have the same length. Here is the fix :

.win-listview .win-listlayout .win-container {
    height: auto;
}
Discovering WinJS : Episode 4 - The SplitView

Discovering WinJS : Episode 4 - The SplitView

Now, we have access to the entire Gitter API thanks to the token provided by OAuth process. To create an app, you absolutely need a UI, and it's based on User Controls. In this article, we'll see how to implement a new control designed for the new OS Windows 10 : the SplitView.

A list of User Controls

If you need a list of existing User Controls in WinJS, I recommend you to follow this link : http://winjs.azurewebsites.net/

There is plenty of examples and there is the most common used controls :

  • the new SplitView
  • ListView
  • Hub
  • AppBar
  • And more than that, like animations

A simple SplitView

Here is the HTML :

<body class="win-type-body" ng-controller="AppCtrl">  
    <win-split-viewclass="splitView" data-win-control="WinJS.UI.SplitView">
        <!-- Pane area -->
        <win-split-view-pane>
            <div class="header">
                <button class="win-splitviewpanetoggle"
                        data-win-control="WinJS.UI.SplitViewPaneToggle"
                        data-win-options="{ splitView: select('.splitView') }"></button>
                <div class="title">Modern Gitter</div>
            </div>

            <div class="nav-commands">
                <div data-win-control="WinJS.UI.NavBarCommand" data-win-options="{ label: 'Home', icon: 'home'}"></div>
                <div data-win-control="WinJS.UI.NavBarCommand" data-win-options="{ label: 'Favorite', icon: 'favorite'}"></div>
                <div data-win-control="WinJS.UI.NavBarCommand" data-win-options="{ label: 'Settings', icon: 'settings'}"></div>
            </div>
        </win-split-view-pane>

        <!-- Content area -->
        <win-split-view-contentclass="content-area">
            <h2 class="win-h2">Odonno/Modern-Gitter</h2>
        </win-split-view-content>
    </win-split-view>
</body>  

Here is the corresponding CSS you need to make it work :

.win-splitviewpanetoggle {
    float: left;
}

.header {
    white-space: nowrap;
}

.title {
    font-size: 25px;
    left: 50px;
    margin-top: 6px;
    vertical-align: middle;
    display: inline-block;
}

.nav-commands {
    margin-top: 20px;
}

.win-navbarcommand-button {
    background-color: transparent;
}

.win-splitview-pane-hidden .win-navbarcommand-button {
    width: 46px;
}

.win-splitview-pane-hidden .win-navbarcommand-label {
    visibility: hidden;
}

.win-splitview-content {
    background-color: rgba(241, 240, 237, 0.2);
}

.content-area {
    margin-left: 20px;
    margin-top: 6px;
}

And finally there is some JS you can put inside your default.js file :

WinJS.UI.processAll().then(function () {  
    // makes the splitView adaptable to screen size
    var splitView = document.querySelector(".splitView").winControl;
    new WinJS.UI._WinKeyboard(splitView.paneElement);
});

Populate with rooms

We'll now see how to configure our SplitView to have custom content based on Gitter API.

Retrieve your rooms

First of all, we need to retrieve data from the API : the list of rooms. For that, I started to create a new angular service that return Promise based on WinJS xhr call.

.service('ApiService', function (ConfigService, OAuthService) {
    var apiService = this;

    apiService.getRooms = function () {
        return new Promise((done, error) => {
            WinJS
                .xhr({
                    url: ConfigService.baseUrl + "rooms",
                    headers: { "Content-type": "application/json", "Authorization": "Bearer " + OAuthService.refreshToken }
                })
                .then(function (success) {
                    done(JSON.parse(success.response));
                });
        });
    };

    return apiService;
})

Show rooms in the pane area

Now, we'll add these rooms in the pane area :

<div class="nav-commands">  
    <win-split-view-command class="room-tile" ng-repeat="room in rooms">
        <img class="room-tile-icon" ng-src="{{ room.image }}" />
        <div ng-show="splitViewWinControl.paneOpened" class="room-tile-text">{{ room.name }}</div>
    </win-split-view-command>
</div>  

Here is the CSS corresponding :

.content-area {
    margin-left: 20px;
    margin-top: 6px;
}

.room-tile {
    max-height: 40px;
}

.room-tile-icon {
    display: inline-block;
    max-height: 24px;
    max-width: 24px;
    margin-left: 12px;
    margin-top: 6px;
}

.room-tile-text {
    display: inline-block;
    vertical-align: top;
    margin-left: 10px;
    margin-top: 8px;
}

You can see I use a ng-show directive on the text (room name). To enable it, you absolutely need to a win-control directive that bind your SplitView control to your controller.

<win-split-view win-control="splitViewWinControl" class="splitView">  

Oh, and forgot, if you don't see room images, it's because I compute the property in my controller :

ApiService.getRooms().then(rooms => {  
    $scope.rooms = rooms;

    // compute room image
    for (var i = 0; i < $scope.rooms.length; i++) {
        if ($scope.rooms[i].user) {
            $scope.rooms[i].image = $scope.rooms[i].user.avatarUrlMedium;
        } else {
            $scope.rooms[i].image = "https://avatars.githubusercontent.com/" + $scope.rooms[i].name.split('/')[0];
        }
    }

    $scope.$apply();
});

Show current room in the content area

And now, it's really simple. Just add an event on the click to show the selected room information.

$scope.selectRoom = function (room) {
    $scope.currentRoom = room;
};
<win-split-view-content class="content-area">  
    <h2 class="win-h2">{{ currentRoom.name }}</h2>
    <h6 class="win-h6 room-topic">{{ currentRoom.topic }}</h6>
</win-split-view-content>  

The result

And here is the result :

Discovering WinJS : Episode 3 - OAuth

Discovering WinJS : Episode 3 - OAuth

Based on Gitter API documentation, we need now to authenticate to Gitter API. Once it is done, we'll be able to retrieve data : rooms, messages, ...

OAuth in WinJS

Thanks to Timmy Kokke, I've done OAuth with WinJS in less than an hour.

Here is the code you need to execute OAuth : https://github.com/sorskoot/UWP-OAuth-demo

And, it's magical, you also have a video here : https://www.youtube.com/watch?v=eogjvTurTN8

I rewrote some code to make it work due to the process of Gitter OAuth you can find here : https://developer.gitter.im/docs/authentication

OAuthService

I wanted to make it simple and safe so, I've created an Angular service only for OAuth : OAuthService.

And here is the result :

angular.module('modern-gitter', ['winjs'])  
    .service('ConfigService', function () {
        var configService = this;

        configService.baseUrl = "https://api.gitter.im/v1/";
        configService.tokenUri = "https://gitter.im/login/oauth/token";
        configService.clientId = "X";
        configService.clientSecret = "X";
        configService.redirectUri = "http://localhost";
        configService.authUri = "https://gitter.im/login/oauth/authorize";

        return configService;
    })
    .service('OAuthService', function (ConfigService) {
        // based on the code of Timmy Kokke (https://github.com/sorskoot/UWP-OAuth-demo)
        var oauthService = this;

        oauthService.refreshToken = '';

        oauthService.initialize = function () {
            oauthService.refreshToken = retrieveTokenFromVault();
        };

        oauthService.connect = function () {
            return new Promise((done, error) => {
                if (!oauthService.refreshToken) {
                    authenticate().then(
                        token => grant(token).then(accessToken => {
                            let cred = new Windows.Security.Credentials
                                .PasswordCredential("OauthToken", "CurrentUser", accessToken.access_token);
                            oauthService.refreshToken = accessToken.access_token;
                            let passwordVault = new Windows.Security.Credentials.PasswordVault();
                            passwordVault.add(cred);
                            done(oauthService.refreshToken);
                        }));
                } else {
                    done(oauthService.refreshToken);
                }
            });
        };

        function retrieveTokenFromVault() {
            let passwordVault = new Windows.Security.Credentials.PasswordVault();
            let storedToken;

            try {
                let credential = passwordVault.retrieve("OauthToken", "CurrentUser");
                storedToken = credential.password;
                // Uncomment this line to remove the token from the password vault so you'll have to log in again
                //passwordVault.remove(credential);
            } catch (e) {
                // no stored credentials
            }

            return storedToken;
        }

        function grant(token) {
            let oauthUrl = ConfigService.tokenUri;
            let clientId = ConfigService.clientId;
            let clientSecret = ConfigService.clientSecret;
            let redirectUrl = ConfigService.redirectUri;

            return WinJS.xhr({
                type: "post",
                url: oauthUrl,
                data: serializeData({
                    code: token,
                    client_id: clientId,
                    client_secret: clientSecret,
                    redirect_uri: redirectUrl,
                    grant_type: 'authorization_code'
                }),
                headers:
                {
                    "Content-type": "application/x-www-form-urlencoded; charset=utf-8"
                }
            }).then(x => JSON.parse(x.response));
        };

        function authenticate() {
            return new Promise(function (complete, error) {
                let oauthUrl = ConfigService.authUri;
                let clientId = ConfigService.clientId;
                let redirectUrl = ConfigService.redirectUri;
                let requestUri = Windows.Foundation.Uri(`${oauthUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUrl)}&response_type=code&access_type=offline`);
                let callbackUri = Windows.Foundation.Uri(redirectUrl);

                Windows.Security.Authentication.Web.WebAuthenticationBroker.
                    authenticateAsync(Windows.Security.Authentication.Web.
                        WebAuthenticationOptions.none, requestUri, callbackUri)
                    .done(result => {
                        if (result.responseStatus === 0) {
                            complete(result.responseData.replace('http://localhost/?code=', ''));
                        } else {
                            error(result);
                        }
                    });
            });
        }

        // Serialize a piece of data to a querystring
        function serializeData(data, encode) {
            if (typeof data !== 'object') {
                return ((data == null) ? "" : data.toString());
            }
            let buffer = [];

            // Serialize each key in the object
            for (let name in data) {
                if (!data.hasOwnProperty(name)) {
                    continue;
                }
                let value = data[name];
                if (!!encode) {
                    buffer.push(`${encodeURIComponent(name)} = ${encodeURIComponent((value == null) ? "" : value)}`);
                } else {
                    buffer.push(`${name}=${value == null ? "" : value}`);
                }
            }

            // Serialize the buffer and clean it up for transportation
            return buffer.join("&").replace(/%20/g, "+");
        }

        return oauthService;
    })
    .controller('AppCtrl', function ($scope, OAuthService) {
        // initialize controller
        OAuthService.initialize();
        OAuthService.connect().then(t => {
            console.log('Sucessfully logged to Gitter API');
        });
    });

If you now launch your application and execute OAuthService connect() method, it will work as expected.