More work on replayable errors
Expose a button that does that retries outgoing messages if possible. // FREEBIE
This commit is contained in:
		
					parent
					
						
							
								bc0c9bd133
							
						
					
				
			
			
				commit
				
					
						a32f3ff1f6
					
				
			
		
					 8 changed files with 131 additions and 55 deletions
				
			
		| 
						 | 
				
			
			@ -248,6 +248,10 @@
 | 
			
		|||
        {{ #conflict }}
 | 
			
		||||
            <button class='conflict'><span>Verify</span></button>
 | 
			
		||||
        {{ /conflict }}
 | 
			
		||||
        {{ #retry }}
 | 
			
		||||
          <button class='retry'><span>Retry</span></button>
 | 
			
		||||
          <span class='error-message'>{{message}}</span>
 | 
			
		||||
        {{ /retry }}
 | 
			
		||||
        {{ #errors }}
 | 
			
		||||
            <div>
 | 
			
		||||
              <span class='error-message'>{{message}}</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,9 +7,9 @@
 | 
			
		|||
 | 
			
		||||
    var registeredFunctions = {};
 | 
			
		||||
    var Type = {
 | 
			
		||||
        SEND_MESSAGE: 1,
 | 
			
		||||
        ENCRYPT_MESSAGE: 1,
 | 
			
		||||
        INIT_SESSION: 2,
 | 
			
		||||
        NETWORK_REQUEST: 3,
 | 
			
		||||
        TRANSMIT_MESSAGE: 3,
 | 
			
		||||
    };
 | 
			
		||||
    window.textsecure = window.textsecure || {};
 | 
			
		||||
    window.textsecure.replay = {
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +48,7 @@
 | 
			
		|||
 | 
			
		||||
    function OutgoingIdentityKeyError(number, message, timestamp, identityKey) {
 | 
			
		||||
        ReplayableError.call(this, {
 | 
			
		||||
            functionCode : Type.SEND_MESSAGE,
 | 
			
		||||
            functionCode : Type.ENCRYPT_MESSAGE,
 | 
			
		||||
            args         : [number, message, timestamp]
 | 
			
		||||
        });
 | 
			
		||||
        this.name = 'OutgoingIdentityKeyError';
 | 
			
		||||
| 
						 | 
				
			
			@ -59,23 +59,40 @@
 | 
			
		|||
    OutgoingIdentityKeyError.prototype = new ReplayableError();
 | 
			
		||||
    OutgoingIdentityKeyError.prototype.constructor = OutgoingIdentityKeyError;
 | 
			
		||||
 | 
			
		||||
    function NetworkError(number, jsonData, legacy, code) {
 | 
			
		||||
    function OutgoingMessageError(number, message, timestamp, httpError) {
 | 
			
		||||
        ReplayableError.call(this, {
 | 
			
		||||
            functionCode : Type.NETWORK_REQUEST,
 | 
			
		||||
            functionCode : Type.ENCRYPT_MESSAGE,
 | 
			
		||||
            args         : [number, message, timestamp]
 | 
			
		||||
        });
 | 
			
		||||
        this.name = 'OutgoingMessageError';
 | 
			
		||||
        if (httpError) {
 | 
			
		||||
            this.code = httpError.code;
 | 
			
		||||
            this.message = httpError.message;
 | 
			
		||||
            this.stack = httpError.stack;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    OutgoingMessageError.prototype = new ReplayableError();
 | 
			
		||||
    OutgoingMessageError.prototype.constructor = OutgoingMessageError;
 | 
			
		||||
 | 
			
		||||
    function SendMessageNetworkError(number, jsonData, legacy, httpError) {
 | 
			
		||||
        ReplayableError.call(this, {
 | 
			
		||||
            functionCode : Type.TRANSMIT_MESSAGE,
 | 
			
		||||
            args         : [number, jsonData, legacy]
 | 
			
		||||
        });
 | 
			
		||||
        this.name = 'NetworkError';
 | 
			
		||||
        this.message = 'Network request failed'
 | 
			
		||||
        this.code = code;
 | 
			
		||||
        this.name = 'SendMessageNetworkError';
 | 
			
		||||
        this.number = number;
 | 
			
		||||
        this.code = httpError.code;
 | 
			
		||||
        this.message = httpError.message;
 | 
			
		||||
        this.stack = httpError.stack;
 | 
			
		||||
    }
 | 
			
		||||
    NetworkError.prototype = new ReplayableError();
 | 
			
		||||
    NetworkError.prototype.constructor = NetworkError;
 | 
			
		||||
    SendMessageNetworkError.prototype = new ReplayableError();
 | 
			
		||||
    SendMessageNetworkError.prototype.constructor = SendMessageNetworkError;
 | 
			
		||||
 | 
			
		||||
    window.textsecure.NetworkError = NetworkError;
 | 
			
		||||
    window.textsecure.SendMessageNetworkError = SendMessageNetworkError;
 | 
			
		||||
    window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError;
 | 
			
		||||
    window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError;
 | 
			
		||||
    window.textsecure.ReplayableError = ReplayableError;
 | 
			
		||||
    window.textsecure.OutgoingMessageError = OutgoingMessageError;
 | 
			
		||||
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39637,14 +39654,14 @@ MessageSender.prototype = {
 | 
			
		|||
            });
 | 
			
		||||
        })).then(function(jsonData) {
 | 
			
		||||
            var legacy = (message instanceof textsecure.protobuf.DataMessage);
 | 
			
		||||
            return this.sendRequest(number, jsonData, legacy);
 | 
			
		||||
            return this.sendMessageRequest(number, jsonData, legacy);
 | 
			
		||||
        }.bind(this));
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    sendRequest: function(number, jsonData, legacy) {
 | 
			
		||||
    sendMessageRequest: function(number, jsonData, legacy) {
 | 
			
		||||
        return this.server.sendMessages(number, jsonData, legacy).catch(function(e) {
 | 
			
		||||
            if (e.name === 'HTTPError' && e.code === -1) {
 | 
			
		||||
                throw new NetworkError(number, jsonData, legacy);
 | 
			
		||||
            if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
 | 
			
		||||
                throw new textsecure.SendMessageNetworkError(number, jsonData, legacy, e);
 | 
			
		||||
            }
 | 
			
		||||
            throw e;
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			@ -39684,9 +39701,10 @@ MessageSender.prototype = {
 | 
			
		|||
        };
 | 
			
		||||
 | 
			
		||||
        var registerError = function(number, reason, error) {
 | 
			
		||||
            if (!error) {
 | 
			
		||||
                error = new Error(reason);
 | 
			
		||||
            if (!error || error.name === 'HTTPError') {
 | 
			
		||||
                error = new textsecure.OutgoingMessageError(number, message.toArrayBuffer(), timestamp, error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            error.number = number;
 | 
			
		||||
            error.reason = reason;
 | 
			
		||||
            errors[errors.length] = error;
 | 
			
		||||
| 
						 | 
				
			
			@ -40025,8 +40043,8 @@ window.textsecure = window.textsecure || {};
 | 
			
		|||
 | 
			
		||||
textsecure.MessageSender = function(url, username, password) {
 | 
			
		||||
    var sender = new MessageSender(url, username, password);
 | 
			
		||||
    textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.SEND_MESSAGE);
 | 
			
		||||
    textsecure.replay.registerFunction(sender.sendRequest.bind(sender), textsecure.replay.Type.NETWORK_REQUEST);
 | 
			
		||||
    textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.ENCRYPT_MESSAGE);
 | 
			
		||||
    textsecure.replay.registerFunction(sender.sendMessageRequest.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE);
 | 
			
		||||
 | 
			
		||||
    this.sendRequestGroupSyncMessage   = sender.sendRequestGroupSyncMessage  .bind(sender);
 | 
			
		||||
    this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage.bind(sender);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -175,8 +175,18 @@
 | 
			
		|||
            this.set({errors: errors});
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        removeOutgoingErrors: function(number) {
 | 
			
		||||
            var errors = _.partition(this.get('errors'), function(e) {
 | 
			
		||||
                return e.number === number &&
 | 
			
		||||
                    (e.name === 'OutgoingMessageError' ||
 | 
			
		||||
                     e.name === 'SendMessageNetworkError');
 | 
			
		||||
            });
 | 
			
		||||
            this.set({errors: errors[1]});
 | 
			
		||||
            return errors[0][0];
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        resend: function(number) {
 | 
			
		||||
            var error = this.getSendError(number);
 | 
			
		||||
            var error = this.removeOutgoingErrors(number);
 | 
			
		||||
            if (error) {
 | 
			
		||||
                var promise = new textsecure.ReplayableError(error).replay();
 | 
			
		||||
                this.send(promise);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,22 +10,33 @@
 | 
			
		|||
        templateName: 'contact-detail',
 | 
			
		||||
        initialize: function(options) {
 | 
			
		||||
            this.conflict = options.conflict;
 | 
			
		||||
            this.retry = _.find(options.errors, function(e) {
 | 
			
		||||
                return (e.name === 'OutgoingMessageError' ||
 | 
			
		||||
                        e.name === 'SendMessageNetworkError');
 | 
			
		||||
            });
 | 
			
		||||
            this.errors = _.reject(options.errors, function(e) {
 | 
			
		||||
                return (e.name === 'IncomingIdentityKeyError' ||
 | 
			
		||||
                        e.name === 'OutgoingIdentityKeyError');
 | 
			
		||||
                        e.name === 'OutgoingIdentityKeyError' ||
 | 
			
		||||
                        e.name === 'OutgoingMessageError' ||
 | 
			
		||||
                        e.name === 'SendMessageNetworkError');
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        events: {
 | 
			
		||||
            'click .conflict': 'triggerConflict'
 | 
			
		||||
            'click .conflict': 'triggerConflict',
 | 
			
		||||
            'click .retry': 'triggerRetry'
 | 
			
		||||
        },
 | 
			
		||||
        triggerConflict: function() {
 | 
			
		||||
            this.$el.trigger('conflict', {conflict: this.conflict});
 | 
			
		||||
        },
 | 
			
		||||
        triggerRetry: function() {
 | 
			
		||||
            this.$el.trigger('retry', {error: this.retry});
 | 
			
		||||
        },
 | 
			
		||||
        render_attributes: function() {
 | 
			
		||||
            return {
 | 
			
		||||
                name     : this.model.getTitle(),
 | 
			
		||||
                avatar   : this.model.getAvatar(),
 | 
			
		||||
                conflict : this.conflict,
 | 
			
		||||
                retry    : this.retry,
 | 
			
		||||
                errors   : this.errors
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +54,8 @@
 | 
			
		|||
        },
 | 
			
		||||
        events: {
 | 
			
		||||
            'click .back': 'goBack',
 | 
			
		||||
            'conflict': 'conflictDialogue'
 | 
			
		||||
            'conflict': 'conflictDialogue',
 | 
			
		||||
            'retry': 'retryMessage',
 | 
			
		||||
        },
 | 
			
		||||
        goBack: function() {
 | 
			
		||||
            this.trigger('back');
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +94,9 @@
 | 
			
		|||
                this.render();
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        retryMessage: function(e, data) {
 | 
			
		||||
            this.model.resend(data.error.number);
 | 
			
		||||
        },
 | 
			
		||||
        renderContact: function(contact) {
 | 
			
		||||
            var v = new ContactView({
 | 
			
		||||
                model: contact,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,9 +6,9 @@
 | 
			
		|||
 | 
			
		||||
    var registeredFunctions = {};
 | 
			
		||||
    var Type = {
 | 
			
		||||
        SEND_MESSAGE: 1,
 | 
			
		||||
        ENCRYPT_MESSAGE: 1,
 | 
			
		||||
        INIT_SESSION: 2,
 | 
			
		||||
        NETWORK_REQUEST: 3,
 | 
			
		||||
        TRANSMIT_MESSAGE: 3,
 | 
			
		||||
    };
 | 
			
		||||
    window.textsecure = window.textsecure || {};
 | 
			
		||||
    window.textsecure.replay = {
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +47,7 @@
 | 
			
		|||
 | 
			
		||||
    function OutgoingIdentityKeyError(number, message, timestamp, identityKey) {
 | 
			
		||||
        ReplayableError.call(this, {
 | 
			
		||||
            functionCode : Type.SEND_MESSAGE,
 | 
			
		||||
            functionCode : Type.ENCRYPT_MESSAGE,
 | 
			
		||||
            args         : [number, message, timestamp]
 | 
			
		||||
        });
 | 
			
		||||
        this.name = 'OutgoingIdentityKeyError';
 | 
			
		||||
| 
						 | 
				
			
			@ -58,22 +58,39 @@
 | 
			
		|||
    OutgoingIdentityKeyError.prototype = new ReplayableError();
 | 
			
		||||
    OutgoingIdentityKeyError.prototype.constructor = OutgoingIdentityKeyError;
 | 
			
		||||
 | 
			
		||||
    function NetworkError(number, jsonData, legacy, code) {
 | 
			
		||||
    function OutgoingMessageError(number, message, timestamp, httpError) {
 | 
			
		||||
        ReplayableError.call(this, {
 | 
			
		||||
            functionCode : Type.NETWORK_REQUEST,
 | 
			
		||||
            functionCode : Type.ENCRYPT_MESSAGE,
 | 
			
		||||
            args         : [number, message, timestamp]
 | 
			
		||||
        });
 | 
			
		||||
        this.name = 'OutgoingMessageError';
 | 
			
		||||
        if (httpError) {
 | 
			
		||||
            this.code = httpError.code;
 | 
			
		||||
            this.message = httpError.message;
 | 
			
		||||
            this.stack = httpError.stack;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    OutgoingMessageError.prototype = new ReplayableError();
 | 
			
		||||
    OutgoingMessageError.prototype.constructor = OutgoingMessageError;
 | 
			
		||||
 | 
			
		||||
    function SendMessageNetworkError(number, jsonData, legacy, httpError) {
 | 
			
		||||
        ReplayableError.call(this, {
 | 
			
		||||
            functionCode : Type.TRANSMIT_MESSAGE,
 | 
			
		||||
            args         : [number, jsonData, legacy]
 | 
			
		||||
        });
 | 
			
		||||
        this.name = 'NetworkError';
 | 
			
		||||
        this.message = 'Network request failed'
 | 
			
		||||
        this.code = code;
 | 
			
		||||
        this.name = 'SendMessageNetworkError';
 | 
			
		||||
        this.number = number;
 | 
			
		||||
        this.code = httpError.code;
 | 
			
		||||
        this.message = httpError.message;
 | 
			
		||||
        this.stack = httpError.stack;
 | 
			
		||||
    }
 | 
			
		||||
    NetworkError.prototype = new ReplayableError();
 | 
			
		||||
    NetworkError.prototype.constructor = NetworkError;
 | 
			
		||||
    SendMessageNetworkError.prototype = new ReplayableError();
 | 
			
		||||
    SendMessageNetworkError.prototype.constructor = SendMessageNetworkError;
 | 
			
		||||
 | 
			
		||||
    window.textsecure.NetworkError = NetworkError;
 | 
			
		||||
    window.textsecure.SendMessageNetworkError = SendMessageNetworkError;
 | 
			
		||||
    window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError;
 | 
			
		||||
    window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError;
 | 
			
		||||
    window.textsecure.ReplayableError = ReplayableError;
 | 
			
		||||
    window.textsecure.OutgoingMessageError = OutgoingMessageError;
 | 
			
		||||
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,14 +37,14 @@ MessageSender.prototype = {
 | 
			
		|||
            });
 | 
			
		||||
        })).then(function(jsonData) {
 | 
			
		||||
            var legacy = (message instanceof textsecure.protobuf.DataMessage);
 | 
			
		||||
            return this.sendRequest(number, jsonData, legacy);
 | 
			
		||||
            return this.sendMessageRequest(number, jsonData, legacy);
 | 
			
		||||
        }.bind(this));
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    sendRequest: function(number, jsonData, legacy) {
 | 
			
		||||
    sendMessageRequest: function(number, jsonData, legacy) {
 | 
			
		||||
        return this.server.sendMessages(number, jsonData, legacy).catch(function(e) {
 | 
			
		||||
            if (e.name === 'HTTPError' && e.code === -1) {
 | 
			
		||||
                throw new NetworkError(number, jsonData, legacy);
 | 
			
		||||
            if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
 | 
			
		||||
                throw new textsecure.SendMessageNetworkError(number, jsonData, legacy, e);
 | 
			
		||||
            }
 | 
			
		||||
            throw e;
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			@ -84,9 +84,10 @@ MessageSender.prototype = {
 | 
			
		|||
        };
 | 
			
		||||
 | 
			
		||||
        var registerError = function(number, reason, error) {
 | 
			
		||||
            if (!error) {
 | 
			
		||||
                error = new Error(reason);
 | 
			
		||||
            if (!error || error.name === 'HTTPError') {
 | 
			
		||||
                error = new textsecure.OutgoingMessageError(number, message.toArrayBuffer(), timestamp, error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            error.number = number;
 | 
			
		||||
            error.reason = reason;
 | 
			
		||||
            errors[errors.length] = error;
 | 
			
		||||
| 
						 | 
				
			
			@ -425,8 +426,8 @@ window.textsecure = window.textsecure || {};
 | 
			
		|||
 | 
			
		||||
textsecure.MessageSender = function(url, username, password) {
 | 
			
		||||
    var sender = new MessageSender(url, username, password);
 | 
			
		||||
    textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.SEND_MESSAGE);
 | 
			
		||||
    textsecure.replay.registerFunction(sender.sendRequest.bind(sender), textsecure.replay.Type.NETWORK_REQUEST);
 | 
			
		||||
    textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.ENCRYPT_MESSAGE);
 | 
			
		||||
    textsecure.replay.registerFunction(sender.sendMessageRequest.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE);
 | 
			
		||||
 | 
			
		||||
    this.sendRequestGroupSyncMessage   = sender.sendRequestGroupSyncMessage  .bind(sender);
 | 
			
		||||
    this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage.bind(sender);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,13 +61,18 @@
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .conflict {
 | 
			
		||||
  .contact-detail button {
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 0.5em;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    background: #d00;
 | 
			
		||||
    background: $blue;
 | 
			
		||||
 | 
			
		||||
    span {
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
      padding-left: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:before {
 | 
			
		||||
      content: '';
 | 
			
		||||
| 
						 | 
				
			
			@ -75,13 +80,15 @@
 | 
			
		|||
      vertical-align: middle;
 | 
			
		||||
      width: 18px;
 | 
			
		||||
      height: 18px;
 | 
			
		||||
      background: url('/images/error.png') no-repeat center center;
 | 
			
		||||
      background: url('/images/refresh.png') no-repeat center center;
 | 
			
		||||
      background-size: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .conflict {
 | 
			
		||||
    background: #d00;
 | 
			
		||||
 | 
			
		||||
    span {
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
      padding-left: 5px;
 | 
			
		||||
    &:before {
 | 
			
		||||
      background: url('/images/error.png') no-repeat center center;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -550,24 +550,28 @@ input.search {
 | 
			
		|||
      font-weight: bold;
 | 
			
		||||
      padding-right: 1em;
 | 
			
		||||
      vertical-align: top; }
 | 
			
		||||
  .message-detail .conflict {
 | 
			
		||||
  .message-detail .contact-detail button {
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 0.5em;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    background: #d00; }
 | 
			
		||||
    .message-detail .conflict:before {
 | 
			
		||||
    background: #2090ea; }
 | 
			
		||||
    .message-detail .contact-detail button span {
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
      padding-left: 5px; }
 | 
			
		||||
    .message-detail .contact-detail button:before {
 | 
			
		||||
      content: '';
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
      width: 18px;
 | 
			
		||||
      height: 18px;
 | 
			
		||||
      background: url("/images/error.png") no-repeat center center;
 | 
			
		||||
      background: url("/images/refresh.png") no-repeat center center;
 | 
			
		||||
      background-size: 100%; }
 | 
			
		||||
    .message-detail .conflict span {
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
      padding-left: 5px; }
 | 
			
		||||
  .message-detail .conflict {
 | 
			
		||||
    background: #d00; }
 | 
			
		||||
    .message-detail .conflict:before {
 | 
			
		||||
      background: url("/images/error.png") no-repeat center center; }
 | 
			
		||||
 | 
			
		||||
.group-update {
 | 
			
		||||
  font-size: smaller; }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue