+ `;
+}
+
+function secondsToL10nId(seconds) {
+ if (seconds < 3600) {
+ return { id: 'timespanMinutes', num: Math.floor(seconds / 60) };
+ } else if (seconds < 86400) {
+ return { id: 'timespanHours', num: Math.floor(seconds / 3600) };
+ } else {
+ return { id: 'timespanDays', num: Math.floor(seconds / 86400) };
+ }
+}
+
+function timeLeft(milliseconds) {
+ if (milliseconds < 1) {
+ return { id: 'linkExpiredAlt' };
+ }
+ const minutes = Math.floor(milliseconds / 1000 / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+ if (days >= 1) {
+ return {
+ id: 'expiresDaysHoursMinutes',
+ days,
+ hours: hours % 24,
+ minutes: minutes % 60
+ };
+ }
+ if (hours >= 1) {
+ return {
+ id: 'expiresHoursMinutes',
+ hours,
+ minutes: minutes % 60
+ };
+ } else if (hours === 0) {
+ if (minutes === 0) {
+ return { id: 'expiresMinutes', minutes: '< 1' };
+ }
+ return { id: 'expiresMinutes', minutes };
+ }
+ return null;
+}
+
+function platform() {
+ if (typeof Android === 'object') {
+ return 'android';
+ }
+ return 'web';
+}
+
+const ECE_RECORD_SIZE = 1024 * 64;
+const TAG_LENGTH = 16;
+function encryptedSize(size, rs = ECE_RECORD_SIZE, tagLength = TAG_LENGTH) {
+ const chunk_meta = tagLength + 1; // Chunk metadata, tag and delimiter
+ return 21 + size + chunk_meta * Math.ceil(size / (rs - chunk_meta));
+}
+
+let translate = function() {
+ throw new Error('uninitialized translate function. call setTranslate first');
+};
+function setTranslate(t) {
+ translate = t;
+}
+
module.exports = {
+ locale,
fadeOut,
delay,
allowedCopy,
@@ -180,7 +288,14 @@ module.exports = {
arrayToB64,
b64ToArray,
loadShim,
- canHasSend,
isFile,
- openLinksInNewTab
+ openLinksInNewTab,
+ browserName,
+ streamToArrayBuffer,
+ list,
+ secondsToL10nId,
+ timeLeft,
+ platform,
+ encryptedSize,
+ setTranslate
};
diff --git a/app/zip.js b/app/zip.js
new file mode 100644
index 00000000..57d751cb
--- /dev/null
+++ b/app/zip.js
@@ -0,0 +1,186 @@
+import crc32 from 'crc/crc32';
+
+const encoder = new TextEncoder();
+
+function dosDateTime(dateTime = new Date()) {
+ const year = (dateTime.getFullYear() - 1980) << 9;
+ const month = (dateTime.getMonth() + 1) << 5;
+ const day = dateTime.getDate();
+ const date = year | month | day;
+ const hour = dateTime.getHours() << 11;
+ const minute = dateTime.getMinutes() << 5;
+ const second = Math.floor(dateTime.getSeconds() / 2);
+ const time = hour | minute | second;
+
+ return { date, time };
+}
+
+class File {
+ constructor(info) {
+ this.name = encoder.encode(info.name);
+ this.size = info.size;
+ this.bytesRead = 0;
+ this.crc = null;
+ this.dateTime = dosDateTime();
+ }
+
+ get header() {
+ const h = new ArrayBuffer(30 + this.name.byteLength);
+ const v = new DataView(h);
+ v.setUint32(0, 0x04034b50, true); // sig
+ v.setUint16(4, 20, true); // version
+ v.setUint16(6, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
+ v.setUint16(8, 0, true); // compression
+ v.setUint16(10, this.dateTime.time, true); // modified time
+ v.setUint16(12, this.dateTime.date, true); // modified date
+ v.setUint32(14, 0, true); // crc32 (in descriptor)
+ v.setUint32(18, 0, true); // compressed size (in descriptor)
+ v.setUint32(22, 0, true); // uncompressed size (in descriptor)
+ v.setUint16(26, this.name.byteLength, true); // name length
+ v.setUint16(28, 0, true); // extra field length
+ for (let i = 0; i < this.name.byteLength; i++) {
+ v.setUint8(30 + i, this.name[i]);
+ }
+ return new Uint8Array(h);
+ }
+
+ get dataDescriptor() {
+ const dd = new ArrayBuffer(16);
+ const v = new DataView(dd);
+ v.setUint32(0, 0x08074b50, true); // sig
+ v.setUint32(4, this.crc, true); // crc32
+ v.setUint32(8, this.size, true); // compressed size
+ v.setUint32(12, this.size, true); // uncompressed size
+ return new Uint8Array(dd);
+ }
+
+ directoryRecord(offset) {
+ const dr = new ArrayBuffer(46 + this.name.byteLength);
+ const v = new DataView(dr);
+ v.setUint32(0, 0x02014b50, true); // sig
+ v.setUint16(4, 20, true); // version made
+ v.setUint16(6, 20, true); // version required
+ v.setUint16(8, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
+ v.setUint16(10, 0, true); // compression
+ v.setUint16(12, this.dateTime.time, true); // modified time
+ v.setUint16(14, this.dateTime.date, true); // modified date
+ v.setUint32(16, this.crc, true); // crc
+ v.setUint32(20, this.size, true); // compressed size
+ v.setUint32(24, this.size, true); // uncompressed size
+ v.setUint16(28, this.name.byteLength, true); // name length
+ v.setUint16(30, 0, true); // extra length
+ v.setUint16(32, 0, true); // comment length
+ v.setUint16(34, 0, true); // disk number
+ v.setUint16(36, 0, true); // internal file attrs
+ v.setUint32(38, 0, true); // external file attrs
+ v.setUint32(42, offset, true); // file offset
+ for (let i = 0; i < this.name.byteLength; i++) {
+ v.setUint8(46 + i, this.name[i]);
+ }
+ return new Uint8Array(dr);
+ }
+
+ get byteLength() {
+ return this.size + this.name.byteLength + 30 + 16;
+ }
+
+ append(data, controller) {
+ this.bytesRead += data.byteLength;
+ const endIndex = data.byteLength - Math.max(this.bytesRead - this.size, 0);
+ const buf = data.slice(0, endIndex);
+ this.crc = crc32(buf, this.crc);
+ controller.enqueue(buf);
+ if (endIndex < data.byteLength) {
+ return data.slice(endIndex, data.byteLength);
+ }
+ }
+}
+
+function centralDirectory(files, controller) {
+ let directoryOffset = 0;
+ let directorySize = 0;
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ const record = file.directoryRecord(directoryOffset);
+ directoryOffset += file.byteLength;
+ controller.enqueue(record);
+ directorySize += record.byteLength;
+ }
+ controller.enqueue(eod(files.length, directorySize, directoryOffset));
+}
+
+function eod(fileCount, directorySize, directoryOffset) {
+ const e = new ArrayBuffer(22);
+ const v = new DataView(e);
+ v.setUint32(0, 0x06054b50, true); // sig
+ v.setUint16(4, 0, true); // disk number
+ v.setUint16(6, 0, true); // directory disk
+ v.setUint16(8, fileCount, true); // number of records
+ v.setUint16(10, fileCount, true); // total records
+ v.setUint32(12, directorySize, true); // size of directory
+ v.setUint32(16, directoryOffset, true); // offset of directory
+ v.setUint16(20, 0, true); // comment length
+ return new Uint8Array(e);
+}
+
+class ZipStreamController {
+ constructor(files, source) {
+ this.files = files;
+ this.fileIndex = 0;
+ this.file = null;
+ this.reader = source.getReader();
+ this.nextFile();
+ this.extra = null;
+ }
+
+ nextFile() {
+ this.file = this.files[this.fileIndex++];
+ }
+
+ async pull(controller) {
+ if (!this.file) {
+ // end of archive
+ centralDirectory(this.files, controller);
+ return controller.close();
+ }
+ if (this.file.bytesRead === 0) {
+ // beginning of file
+ controller.enqueue(this.file.header);
+ if (this.extra) {
+ this.extra = this.file.append(this.extra, controller);
+ }
+ }
+ if (this.file.bytesRead >= this.file.size) {
+ // end of file
+ controller.enqueue(this.file.dataDescriptor);
+ this.nextFile();
+ return this.pull(controller);
+ }
+ const data = await this.reader.read();
+ if (data.done) {
+ this.nextFile();
+ return this.pull(controller);
+ }
+ this.extra = this.file.append(data.value, controller);
+ }
+}
+
+export default class Zip {
+ constructor(manifest, source) {
+ this.files = manifest.files.map(info => new File(info));
+ this.source = source;
+ }
+
+ get stream() {
+ return new ReadableStream(new ZipStreamController(this.files, this.source));
+ }
+
+ get size() {
+ const entries = this.files.reduce(
+ (total, file) => total + file.byteLength * 2 - file.size,
+ 0
+ );
+ const eod = 22;
+ return entries + eod;
+ }
+}
diff --git a/assets/add.svg b/assets/add.svg
new file mode 100644
index 00000000..84db8c30
--- /dev/null
+++ b/assets/add.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/addfiles.svg b/assets/addfiles.svg
new file mode 100644
index 00000000..3d6749d9
--- /dev/null
+++ b/assets/addfiles.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/android-chrome-192x192.png b/assets/android-chrome-192x192.png
new file mode 100644
index 00000000..044cba97
Binary files /dev/null and b/assets/android-chrome-192x192.png differ
diff --git a/assets/android-chrome-512x512.png b/assets/android-chrome-512x512.png
new file mode 100644
index 00000000..553bda11
Binary files /dev/null and b/assets/android-chrome-512x512.png differ
diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png
new file mode 100644
index 00000000..ddb56ec1
Binary files /dev/null and b/assets/apple-touch-icon.png differ
diff --git a/assets/bg.svg b/assets/bg.svg
new file mode 100644
index 00000000..02457110
--- /dev/null
+++ b/assets/bg.svg
@@ -0,0 +1,31 @@
+
+
\ No newline at end of file
diff --git a/assets/blue_file.svg b/assets/blue_file.svg
new file mode 100644
index 00000000..2945487f
--- /dev/null
+++ b/assets/blue_file.svg
@@ -0,0 +1,24 @@
+
+
diff --git a/assets/check-16-blue.svg b/assets/check-16-blue.svg
deleted file mode 100644
index a2633292..00000000
--- a/assets/check-16-blue.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/check-16.svg b/assets/check-16.svg
deleted file mode 100644
index 4243e0dc..00000000
--- a/assets/check-16.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/close-16.svg b/assets/close-16.svg
index 21207a15..9a9da6d3 100644
--- a/assets/close-16.svg
+++ b/assets/close-16.svg
@@ -1 +1 @@
-
+
diff --git a/assets/completed.svg b/assets/completed.svg
new file mode 100644
index 00000000..4c693d3d
--- /dev/null
+++ b/assets/completed.svg
@@ -0,0 +1,260 @@
+
+
diff --git a/assets/copy-16.svg b/assets/copy-16.svg
index 507f199f..56bed4a8 100644
--- a/assets/copy-16.svg
+++ b/assets/copy-16.svg
@@ -1 +1,5 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/cryptofill.js b/assets/cryptofill.js
deleted file mode 100644
index 0b89ad59..00000000
--- a/assets/cryptofill.js
+++ /dev/null
@@ -1,17 +0,0 @@
-var liner=function(e){function r(n){if(t[n])return t[n].exports;var a=t[n]={i:n,l:!1,exports:{}};return e[n].call(a.exports,a,a.exports,r),a.l=!0,a.exports}var t={};return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r.p="",r(r.s=16)}([function(e,r,t){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),function(e){function n(e){for(var r,t=e,n=/[^%](%\d+)/g,a=[];r=n.exec(t);)a.push({arg:r[1],index:r.index});for(var o=a.length-1;o>=0;o--){var i=a[o],s=i.arg.substring(1),c=i.index+1;t=t.substring(0,c)+arguments[+s]+t.substring(c+1+s.length)}return t=t.replace("%%","%")}function a(e){var r;r="string"==typeof e?{name:e}:e,p.checkAlgorithm(r);var t=e;return t.hash&&(t.hash=a(t.hash)),r}function o(e,r){if(!e)throw new s("Parameter '"+r+"' is required and cant be empty");if("undefined"!=typeof Buffer&&Buffer.isBuffer(e))return new Uint8Array(e);if(ArrayBuffer.isView(e)){var t=e.map(function(e){return e});return new Uint8Array(t.buffer)}if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new s("Incoming parameter '"+r+"' has wrong data type. Must be ArrayBufferView or ArrayBuffer")}t.d(r,"WebCryptoError",function(){return s}),t.d(r,"AlgorithmError",function(){return c}),t.d(r,"CryptoKeyError",function(){return u}),t.d(r,"PrepareAlgorithm",function(){return a}),t.d(r,"PrepareData",function(){return o}),t.d(r,"BaseCrypto",function(){return p}),t.d(r,"AlgorithmNames",function(){return h}),t.d(r,"Base64Url",function(){return y}),t.d(r,"SubtleCrypto",function(){return H}),t.d(r,"Aes",function(){return A}),t.d(r,"AesAlgorithmError",function(){return m}),t.d(r,"AesWrapKey",function(){return w}),t.d(r,"AesEncrypt",function(){return v}),t.d(r,"AesECB",function(){return g}),t.d(r,"AesCBC",function(){return C}),t.d(r,"AesCTR",function(){return k}),t.d(r,"AesGCM",function(){return d}),t.d(r,"AesKW",function(){return b}),t.d(r,"RsaKeyGenParamsError",function(){return G}),t.d(r,"RsaHashedImportParamsError",function(){return M}),t.d(r,"Rsa",function(){return B}),t.d(r,"RsaSSA",function(){return T}),t.d(r,"RsaPSSParamsError",function(){return D}),t.d(r,"RsaPSS",function(){return j}),t.d(r,"RsaOAEPParamsError",function(){return W}),t.d(r,"RsaOAEP",function(){return x}),t.d(r,"EcKeyGenParamsError",function(){return U}),t.d(r,"Ec",function(){return _}),t.d(r,"EcAlgorithmError",function(){return K}),t.d(r,"EcDSA",function(){return S}),t.d(r,"EcDH",function(){return O}),t.d(r,"ShaAlgorithms",function(){return E}),t.d(r,"Sha",function(){return P});var i=t(8),s=function(e){function r(r){for(var t=[],a=1;a0&&e.length<=128))throw new m(m.PARAM_WRONG_VALUE,"length","number [1-128]")},r.ALG_NAME=h.AesCTR,r}(v),d=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return Object(i.a)(r,e),r.checkAlgorithmParams=function(e){if(this.checkAlgorithm(e),e.additionalData&&!(ArrayBuffer.isView(e.additionalData)||e.additionalData instanceof ArrayBuffer))throw new m(m.PARAM_WRONG_TYPE,"additionalData","ArrayBufferView or ArrayBuffer");if(!e.iv)throw new m(m.PARAM_REQUIRED,"iv");if(!(ArrayBuffer.isView(e.iv)||e.iv instanceof ArrayBuffer))throw new m(m.PARAM_WRONG_TYPE,"iv","ArrayBufferView or ArrayBuffer");if(e.tagLength){if(![32,64,96,104,112,120,128].some(function(r){return r===e.tagLength}))throw new m(m.PARAM_WRONG_VALUE,"tagLength","32, 64, 96, 104, 112, 120 or 128")}},r.ALG_NAME=h.AesGCM,r}(v),b=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return Object(i.a)(r,e),r.checkAlgorithmParams=function(e){this.checkAlgorithm(e)},r.ALG_NAME=h.AesKW,r.KEY_USAGES=["wrapKey","unwrapKey"],r}(w),E=[h.Sha1,h.Sha256,h.Sha384,h.Sha512].join(" | "),P=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return Object(i.a)(r,e),r.checkAlgorithm=function(r){var t;switch(t="string"==typeof r?{name:r}:r,e.checkAlgorithm.call(this,t),t.name.toUpperCase()){case h.Sha1:case h.Sha256:case h.Sha384:case h.Sha512:break;default:throw new c(c.WRONG_ALG_NAME,t.name,E)}},r.digest=function(e,r){var t=this;return new Promise(function(r,n){t.checkAlgorithm(e),r(void 0)})},r}(p),U=function(e){function r(){var r=null!==e&&e.apply(this,arguments)||this;return r.code=9,r}return Object(i.a)(r,e),r}(c),_=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return Object(i.a)(r,e),r.checkAlgorithm=function(e){if(e.name.toUpperCase()!==this.ALG_NAME.toUpperCase())throw new c(c.WRONG_ALG_NAME,e.name,this.ALG_NAME)},r.checkKeyGenParams=function(e){if(!e.namedCurve)throw new U(U.PARAM_REQUIRED,"namedCurve");if("string"!=typeof e.namedCurve)throw new U(U.PARAM_WRONG_TYPE,"namedCurve","string");switch(e.namedCurve.toUpperCase()){case"P-256":case"K-256":case"P-384":case"P-521":break;default:throw new U(U.PARAM_WRONG_VALUE,"namedCurve","K-256, P-256, P-384 or P-521")}},r.checkKeyGenUsages=function(e){var r=this;e.forEach(function(e){var t=0;for(t;t0&&e.length<=512))throw new c(c.PARAM_WRONG_VALUE,"length","more 0 and less than 512")},r.checkKeyGenUsages=function(e){var r=this;this.checkKeyUsages(e),e.forEach(function(e){var t=0;for(t;t=256&&r<=16384)||r%8)throw new G(G.PARAM_WRONG_VALUE,"modulusLength"," a multiple of 8 bits and >= 256 and <= 16384");var t=e.publicExponent;if(!t)throw new G(G.PARAM_REQUIRED,"publicExponent");if(!ArrayBuffer.isView(t))throw new G(G.PARAM_WRONG_TYPE,"publicExponent","ArrayBufferView");if(3!==t[0]&&(1!==t[0]||0!==t[1]||1!==t[2]))throw new G(G.PARAM_WRONG_VALUE,"publicExponent","Uint8Array([3]) | Uint8Array([1, 0, 1])");if(!e.hash)throw new G(G.PARAM_REQUIRED,"hash",E);P.checkAlgorithm(e.hash)},r.checkKeyGenUsages=function(e){var r=this;this.checkKeyUsages(e),e.forEach(function(e){var t=0;for(t;t>>16&65535,n=65535&e,a=r>>>16&65535,o=65535&r;return n*o+(t*o+n*a<<16>>>0)|0})},function(e,r,t){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n=function(){function e(e){this.algorithm=e.algorithm,e.type&&(this.type=e.type),this.extractable=e.extractable,this.usages=e.usages}return e}();r.CryptoKey=n},function(e,r,t){"use strict";function n(){var e,t={name:"Unknown",version:"0"},n=self.navigator.userAgent;return(e=/edge\/([\d\.]+)/i.exec(n))?(t.name=r.Browser.Edge,t.version=e[1]):/msie/i.test(n)?(t.name=r.Browser.IE,t.version=/msie ([\d\.]+)/i.exec(n)[1]):/Trident/i.test(n)?(t.name=r.Browser.IE,t.version=/rv:([\d\.]+)/i.exec(n)[1]):/chrome/i.test(n)?(t.name=r.Browser.Chrome,t.version=/chrome\/([\d\.]+)/i.exec(n)[1]):/mobile/i.test(n)?(t.name=r.Browser.Mobile,t.version=/mobile\/([\w]+)/i.exec(n)[1]):/safari/i.test(n)?(t.name=r.Browser.Safari,t.version=/version\/([\d\.]+)/i.exec(n)[1]):/firefox/i.test(n)&&(t.name=r.Browser.Firefox,t.version=/firefox\/([\d\.]+)/i.exec(n)[1]),t}function a(e){for(var r=new Uint8Array(e.length),t=0;t-1&&("public"===e.type||"secret"===e.type)&&e.usages.push(t)}),["sign","decrypt","unwrapKey","deriveKey","deriveBits"].forEach(function(t){r.indexOf(t)>-1&&("private"===e.type||"secret"===e.type)&&e.usages.push(t)})))})}function s(e,r,t){if(r&&w.BrowserInfo().name===w.Browser.IE){"extractable"in e&&(e.ext=e.extractable,delete e.extractable);var n=null;switch(r.name.toUpperCase()){case h.AlgorithmNames.AesECB.toUpperCase():case h.AlgorithmNames.AesCBC.toUpperCase():case h.AlgorithmNames.AesGCM.toUpperCase():n=v.AesCrypto;break;default:throw new m.LinerError(m.LinerError.UNSUPPORTED_ALGORITHM,r.name.toUpperCase())}n&&!e.alg&&(e.alg=n.alg2jwk(r)),"key_ops"in e||(e.key_ops=t)}}function c(e){w.BrowserInfo().name===w.Browser.IE&&("ext"in e&&(e.extractable=e.ext,delete e.ext),delete e.key_ops,delete e.alg)}function u(e){var r=/AppleWebKit\/(\d+)/.exec(self.navigator.userAgent);return e.toUpperCase()===h.AlgorithmNames.RsaOAEP&&r&&parseInt(r[1],10)<604}var p=this&&this.__extends||function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,r){e.__proto__=r}||function(e,r){for(var t in r)r.hasOwnProperty(t)&&(e[t]=r[t])};return function(r,t){function n(){this.constructor=r}e(r,t),r.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}}();Object.defineProperty(r,"__esModule",{value:!0});var h=t(0),f=t(0),y=t(0),l=t(3),A=t(2),m=t(1),w=t(5),v=t(11),g=t(12),C=t(13),k=t(14),d=t(15),b=[],E=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return p(r,e),r.prototype.generateKey=function(r,t,n){var o,s=this,c=arguments;return e.prototype.generateKey.apply(this,c).then(function(e){if(o=y.PrepareAlgorithm(r),!(w.BrowserInfo().name===w.Browser.Edge&&o.name.toUpperCase()===h.AlgorithmNames.AesGCM||u(o.name))&&l.nativeSubtle)try{return l.nativeSubtle.generateKey.apply(l.nativeSubtle,c).catch(function(e){w.warn("WebCrypto: native generateKey for "+o.name+" doesn't work.",e&&e.message||"Unknown message")})}catch(e){w.warn("WebCrypto: native generateKey for "+o.name+" doesn't work.",e&&e.message||"Unknown message")}}).then(function(e){if(e){var c=Promise.resolve(e);if(w.BrowserInfo().name===w.Browser.Safari&&(o.name.toUpperCase()===h.AlgorithmNames.EcDH.toUpperCase()||o.name.toUpperCase()===h.AlgorithmNames.EcDSA.toUpperCase())){var u=e.publicKey;c=c.then(function(){return s.exportKey("jwk",u).then(function(a){return s.exportKey("spki",u).then(function(o){for(var i=h.Base64Url.decode(a.x),c=h.Base64Url.decode(a.y),u=i.length+c.length,p=new Uint8Array(o),f=0;f>>7);return n^=99}r||function(){t=[],e=[];var s,i,n=1;for(s=0;s<255;s++)t[s]=n,i=128&n,n<<=1,n&=255,128===i&&(n^=27),n^=t[s],e[t[s]]=s;t[255]=t[0],e[0]=0,r=!0}(),i=[],n=[],a=[[],[],[],[]],h=[[],[],[],[]];for(var c=0;c<256;c++){var u=o(c);i[c]=u,n[u]=c,a[0][c]=s(2,u)<<24|u<<16|u<<8|s(3,u),h[0][u]=s(14,c)<<24|s(9,c)<<16|s(13,c)<<8|s(11,c);for(var f=1;f<4;f++)a[f][c]=a[f-1][c]>>>8|a[f-1][c]<<24,h[f][u]=h[f-1][u]>>>8|h[f-1][u]<<24}}var c=function(t,e){o();var r=new Uint32Array(e);r.set(i,512),r.set(n,768);for(var s=0;s<4;s++)r.set(a[s],4096+1024*s>>2),r.set(h[s],8192+1024*s>>2);var c=function(t,e,r){"use asm";var s=0,i=0,n=0,a=0,h=0,o=0,c=0,u=0,f=0,l=0,p=0,w=0,y=0,_=0,d=0,A=0,v=0,x=0,E=0,g=0,m=0;var b=new t.Uint32Array(r),S=new t.Uint8Array(r);function C(t,e,r,h,o,c,u,f){t=t|0;e=e|0;r=r|0;h=h|0;o=o|0;c=c|0;u=u|0;f=f|0;var l=0,p=0,w=0,y=0,_=0,d=0,A=0,v=0;l=r|0x400,p=r|0x800,w=r|0xc00;o=o^b[(t|0)>>2],c=c^b[(t|4)>>2],u=u^b[(t|8)>>2],f=f^b[(t|12)>>2];for(v=16;(v|0)<=h<<4;v=v+16|0){y=b[(r|o>>22&1020)>>2]^b[(l|c>>14&1020)>>2]^b[(p|u>>6&1020)>>2]^b[(w|f<<2&1020)>>2]^b[(t|v|0)>>2],_=b[(r|c>>22&1020)>>2]^b[(l|u>>14&1020)>>2]^b[(p|f>>6&1020)>>2]^b[(w|o<<2&1020)>>2]^b[(t|v|4)>>2],d=b[(r|u>>22&1020)>>2]^b[(l|f>>14&1020)>>2]^b[(p|o>>6&1020)>>2]^b[(w|c<<2&1020)>>2]^b[(t|v|8)>>2],A=b[(r|f>>22&1020)>>2]^b[(l|o>>14&1020)>>2]^b[(p|c>>6&1020)>>2]^b[(w|u<<2&1020)>>2]^b[(t|v|12)>>2];o=y,c=_,u=d,f=A}s=b[(e|o>>22&1020)>>2]<<24^b[(e|c>>14&1020)>>2]<<16^b[(e|u>>6&1020)>>2]<<8^b[(e|f<<2&1020)>>2]^b[(t|v|0)>>2],i=b[(e|c>>22&1020)>>2]<<24^b[(e|u>>14&1020)>>2]<<16^b[(e|f>>6&1020)>>2]<<8^b[(e|o<<2&1020)>>2]^b[(t|v|4)>>2],n=b[(e|u>>22&1020)>>2]<<24^b[(e|f>>14&1020)>>2]<<16^b[(e|o>>6&1020)>>2]<<8^b[(e|c<<2&1020)>>2]^b[(t|v|8)>>2],a=b[(e|f>>22&1020)>>2]<<24^b[(e|o>>14&1020)>>2]<<16^b[(e|c>>6&1020)>>2]<<8^b[(e|u<<2&1020)>>2]^b[(t|v|12)>>2]}function M(t,e,r,s){t=t|0;e=e|0;r=r|0;s=s|0;C(0x0000,0x0800,0x1000,m,t,e,r,s)}function U(t,e,r,s){t=t|0;e=e|0;r=r|0;s=s|0;var n=0;C(0x0400,0x0c00,0x2000,m,t,s,r,e);n=i,i=a,a=n}function H(t,e,r,f){t=t|0;e=e|0;r=r|0;f=f|0;C(0x0000,0x0800,0x1000,m,h^t,o^e,c^r,u^f);h=s,o=i,c=n,u=a}function T(t,e,r,f){t=t|0;e=e|0;r=r|0;f=f|0;var l=0;C(0x0400,0x0c00,0x2000,m,t,f,r,e);l=i,i=a,a=l;s=s^h,i=i^o,n=n^c,a=a^u;h=t,o=e,c=r,u=f}function D(t,e,r,f){t=t|0;e=e|0;r=r|0;f=f|0;C(0x0000,0x0800,0x1000,m,h,o,c,u);h=s=s^t,o=i=i^e,c=n=n^r,u=a=a^f}function k(t,e,r,f){t=t|0;e=e|0;r=r|0;f=f|0;C(0x0000,0x0800,0x1000,m,h,o,c,u);s=s^t,i=i^e,n=n^r,a=a^f;h=t,o=e,c=r,u=f}function G(t,e,r,f){t=t|0;e=e|0;r=r|0;f=f|0;C(0x0000,0x0800,0x1000,m,h,o,c,u);h=s,o=i,c=n,u=a;s=s^t,i=i^e,n=n^r,a=a^f}function I(t,e,r,h){t=t|0;e=e|0;r=r|0;h=h|0;C(0x0000,0x0800,0x1000,m,f,l,p,w);w=~A&w|A&w+1;p=~d&p|d&p+((w|0)==0);l=~_&l|_&l+((p|0)==0);f=~y&f|y&f+((l|0)==0);s=s^t;i=i^e;n=n^r;a=a^h}function Z(t,e,r,s){t=t|0;e=e|0;r=r|0;s=s|0;var i=0,n=0,a=0,f=0,l=0,p=0,w=0,y=0,_=0,d=0;t=t^h,e=e^o,r=r^c,s=s^u;i=v|0,n=x|0,a=E|0,f=g|0;for(;(_|0)<128;_=_+1|0){if(i>>>31){l=l^t,p=p^e,w=w^r,y=y^s}i=i<<1|n>>>31,n=n<<1|a>>>31,a=a<<1|f>>>31,f=f<<1;d=s&1;s=s>>>1|r<<31,r=r>>>1|e<<31,e=e>>>1|t<<31,t=t>>>1;if(d)t=t^0xe1000000}h=l,o=p,c=w,u=y}function P(t){t=t|0;m=t}function B(t,e,r,h){t=t|0;e=e|0;r=r|0;h=h|0;s=t,i=e,n=r,a=h}function z(t,e,r,s){t=t|0;e=e|0;r=r|0;s=s|0;h=t,o=e,c=r,u=s}function O(t,e,r,s){t=t|0;e=e|0;r=r|0;s=s|0;f=t,l=e,p=r,w=s}function q(t,e,r,s){t=t|0;e=e|0;r=r|0;s=s|0;y=t,_=e,d=r,A=s}function L(t,e,r,s){t=t|0;e=e|0;r=r|0;s=s|0;w=~A&w|A&s,p=~d&p|d&r,l=~_&l|_&e,f=~y&f|y&t}function R(t){t=t|0;if(t&15)return-1;S[t|0]=s>>>24,S[t|1]=s>>>16&255,S[t|2]=s>>>8&255,S[t|3]=s&255,S[t|4]=i>>>24,S[t|5]=i>>>16&255,S[t|6]=i>>>8&255,S[t|7]=i&255,S[t|8]=n>>>24,S[t|9]=n>>>16&255,S[t|10]=n>>>8&255,S[t|11]=n&255,S[t|12]=a>>>24,S[t|13]=a>>>16&255,S[t|14]=a>>>8&255,S[t|15]=a&255;return 16}function K(t){t=t|0;if(t&15)return-1;S[t|0]=h>>>24,S[t|1]=h>>>16&255,S[t|2]=h>>>8&255,S[t|3]=h&255,S[t|4]=o>>>24,S[t|5]=o>>>16&255,S[t|6]=o>>>8&255,S[t|7]=o&255,S[t|8]=c>>>24,S[t|9]=c>>>16&255,S[t|10]=c>>>8&255,S[t|11]=c&255,S[t|12]=u>>>24,S[t|13]=u>>>16&255,S[t|14]=u>>>8&255,S[t|15]=u&255;return 16}function N(){M(0,0,0,0);v=s,x=i,E=n,g=a}function F(t,e,r){t=t|0;e=e|0;r=r|0;var h=0;if(e&15)return-1;while((r|0)>=16){V[t&7](S[e|0]<<24|S[e|1]<<16|S[e|2]<<8|S[e|3],S[e|4]<<24|S[e|5]<<16|S[e|6]<<8|S[e|7],S[e|8]<<24|S[e|9]<<16|S[e|10]<<8|S[e|11],S[e|12]<<24|S[e|13]<<16|S[e|14]<<8|S[e|15]);S[e|0]=s>>>24,S[e|1]=s>>>16&255,S[e|2]=s>>>8&255,S[e|3]=s&255,S[e|4]=i>>>24,S[e|5]=i>>>16&255,S[e|6]=i>>>8&255,S[e|7]=i&255,S[e|8]=n>>>24,S[e|9]=n>>>16&255,S[e|10]=n>>>8&255,S[e|11]=n&255,S[e|12]=a>>>24,S[e|13]=a>>>16&255,S[e|14]=a>>>8&255,S[e|15]=a&255;h=h+16|0,e=e+16|0,r=r-16|0}return h|0}function j(t,e,r){t=t|0;e=e|0;r=r|0;var s=0;if(e&15)return-1;while((r|0)>=16){W[t&1](S[e|0]<<24|S[e|1]<<16|S[e|2]<<8|S[e|3],S[e|4]<<24|S[e|5]<<16|S[e|6]<<8|S[e|7],S[e|8]<<24|S[e|9]<<16|S[e|10]<<8|S[e|11],S[e|12]<<24|S[e|13]<<16|S[e|14]<<8|S[e|15]);s=s+16|0,e=e+16|0,r=r-16|0}return s|0}var V=[M,U,H,T,D,k,G,I];var W=[H,Z];return{set_rounds:P,set_state:B,set_iv:z,set_nonce:O,set_mask:q,set_counter:L,get_state:R,get_iv:K,gcm_init:N,cipher:F,mac:j}}({Uint8Array:Uint8Array,Uint32Array:Uint32Array},t,e);return c.set_key=function(t,e,s,n,a,o,u,f,l){var p=r.subarray(0,60),w=r.subarray(256,316);p.set([e,s,n,a,o,u,f,l]);for(var y=t,_=1;y<4*t+28;y++){var d=p[y-1];(y%t==0||8===t&&y%t==4)&&(d=i[d>>>24]<<24^i[d>>>16&255]<<16^i[d>>>8&255]<<8^i[255&d]),y%t==0&&(d=d<<8^d>>>24^_<<24,_=_<<1^(128&_?27:0)),p[y]=p[y-t]^d}for(var A=0;A=y-4?d:h[0][i[d>>>24]]^h[1][i[d>>>16&255]]^h[2][i[d>>>8&255]]^h[3][i[255&d]];c.set_rounds(t+5)},c};return c.ENC={ECB:0,CBC:2,CFB:4,OFB:6,CTR:7},c.DEC={ECB:1,CBC:3,CFB:5,OFB:6,CTR:7},c.MAC={CBC:0,GCM:1},c.HEAP_DATA=16384,c}(),r=new Uint8Array(1048576),s=e(null,r.buffer);function i(){var t=Error.apply(this,arguments);this.message=t.message,this.stack=t.stack}function n(){var t=Error.apply(this,arguments);this.message=t.message,this.stack=t.stack}function a(){var t=Error.apply(this,arguments);this.message=t.message,this.stack=t.stack}function h(t,e){e=!!e;for(var r=t.length,s=new Uint8Array(e?4*r:r),i=0,n=0;i=r)throw new Error("Malformed string, low surrogate expected at position "+i);a=(55296^a)<<10|65536|56320^t.charCodeAt(i)}else if(!e&&a>>>8)throw new Error("Wide characters are not allowed.");!e||a<=127?s[n++]=a:a<=2047?(s[n++]=192|a>>6,s[n++]=128|63&a):a<=65535?(s[n++]=224|a>>12,s[n++]=128|a>>6&63,s[n++]=128|63&a):(s[n++]=240|a>>18,s[n++]=128|a>>12&63,s[n++]=128|a>>6&63,s[n++]=128|63&a)}return s.subarray(0,n)}function o(t){for(var e="",r=0;r=192&&a<224&&i+1=224&&a<240&&i+2=240&&a<248&&i+3>10,s[n++]=56320|1023&h)}}var o="";for(i=0;i>2,r.getUint32(0),r.getUint32(4),r.getUint32(8),r.getUint32(12),e>16?r.getUint32(16):0,e>16?r.getUint32(20):0,e>24?r.getUint32(24):0,e>24?r.getUint32(28):0),this.key=t}else if(!this.key)throw new Error("key is required")}AES_CTR_set_options(t,e,r){if(void 0!==r){if(r<8||r>48)throw new n("illegal counter size");this.counterSize=r;var s=Math.pow(2,r)-1;this.asm.set_mask(0,0,s/4294967296|0,0|s)}else this.counterSize=r=48,this.asm.set_mask(0,0,65535,4294967295);if(void 0===t)throw new Error("nonce is required");if(!p(t))throw new TypeError("unexpected nonce type");var i=t.length;if(!i||i>16)throw new n("illegal nonce size");this.nonce=t;var a=new DataView(new ArrayBuffer(16));if(new Uint8Array(a.buffer).set(t),this.asm.set_nonce(a.getUint32(0),a.getUint32(4),a.getUint32(8),a.getUint32(12)),void 0!==e){if(!u(e))throw new TypeError("unexpected counter type");if(e<0||e>=Math.pow(2,r))throw new n("illegal counter value");this.counter=e,this.asm.set_counter(0,0,e/4294967296|0,0|e)}else this.counter=0}AES_set_iv(t){if(void 0!==t){if(!p(t))throw new TypeError("unexpected iv type");if(16!==t.length)throw new n("illegal iv size");var e=new DataView(t.buffer,t.byteOffset,t.byteLength);this.iv=t,this.asm.set_iv(e.getUint32(0),e.getUint32(4),e.getUint32(8),e.getUint32(12))}else this.iv=null,this.asm.set_iv(0,0,0,0)}AES_set_padding(t){this.padding=void 0===t||!!t}AES_reset(t,e,r){return this.result=null,this.pos=0,this.len=0,this.AES_set_key(t),this.AES_set_iv(e),this.AES_set_padding(r),this}AES_Encrypt_process(t){if(!p(t))throw new TypeError("data isn't of expected type");for(var r=this.asm,s=this.heap,i=e.ENC[this.mode],n=e.HEAP_DATA,a=this.pos,h=this.len,o=0,c=t.length||0,u=0,f=0,l=new Uint8Array(h+c&-16);c>0;)h+=f=y(s,a+h,t,o,c),o+=f,c-=f,(f=r.cipher(i,n+a,h))&&l.set(s.subarray(a,a+f),u),u+=f,f0;)h+=w=y(s,a+h,t,o,c),o+=w,c-=w,(w=r.cipher(i,n+a,h-(c?0:l)))&&_.set(s.subarray(a,a+w),u),u+=w,w0){if(f%16){if(this.hasOwnProperty("padding"))throw new n("data length must be a multiple of the block size");f+=16-f%16}if(i.cipher(o,c+u,f),this.hasOwnProperty("padding")&&this.padding){var p=h[u+l-1];if(p<1||p>16||p>l)throw new a("bad padding");for(var w=0,y=p;y>1;y--)w|=p^h[u+l-y];if(w)throw new a("bad padding");l-=p}}var _=new Uint8Array(s+l);return s>0&&_.set(r),l>0&&_.set(h.subarray(u,u+l),s),this.result=_,this.pos=0,this.len=0,this}}var d=68719476704;class A extends _{constructor(t,e,r,s,i,n){super(t,void 0,!1,i,n),this.nonce=null,this.adata=null,this.iv=null,this.counter=1,this.tagSize=16,this.mode="GCM",this.BLOCK_SIZE=16,this.reset(t,s,e,r)}reset(t,e,r,s){return this.AES_GCM_reset(t,e,r,s)}encrypt(t){return this.AES_GCM_encrypt(t)}decrypt(t){return this.AES_GCM_decrypt(t)}AES_GCM_Encrypt_process(t){if(!p(t))throw new TypeError("data isn't of expected type");var r=0,s=t.length||0,i=this.asm,n=this.heap,a=this.counter,h=this.pos,o=this.len,c=0,u=o+s&-16,f=0;if((a-1<<4)+o+s>d)throw new RangeError("counter overflow");for(var l=new Uint8Array(u);s>0;)o+=f=y(n,h+o,t,r,s),r+=f,s-=f,f=i.cipher(e.ENC.CTR,e.HEAP_DATA+h,o),(f=i.mac(e.MAC.GCM,e.HEAP_DATA+h,f))&&l.set(n.subarray(h,h+f),c),a+=f>>>4,c+=f,f>>29,r[4]=u>>>21,r[5]=u>>>13&255,r[6]=u>>>5&255,r[7]=u<<3&255,r[8]=r[9]=r[10]=0,r[11]=f>>>29,r[12]=f>>>21&255,r[13]=f>>>13&255,r[14]=f>>>5&255,r[15]=f<<3&255,t.mac(e.MAC.GCM,e.HEAP_DATA,16),t.get_iv(e.HEAP_DATA),t.set_counter(0,0,0,this.gamma0),t.cipher(e.ENC.CTR,e.HEAP_DATA,16),o.set(r.subarray(0,i),h),this.result=o,this.counter=1,this.pos=0,this.len=0,this}AES_GCM_Decrypt_process(t){if(!p(t))throw new TypeError("data isn't of expected type");var r=0,s=t.length||0,i=this.asm,n=this.heap,a=this.counter,h=this.tagSize,o=this.pos,c=this.len,u=0,f=c+s>h?c+s-h&-16:0,l=c+s-f,w=0;if((a-1<<4)+c+s>d)throw new RangeError("counter overflow");for(var _=new Uint8Array(f);s>l;)c+=w=y(n,o+c,t,r,s-l),r+=w,s-=w,w=i.mac(e.MAC.GCM,e.HEAP_DATA+o,w),(w=i.cipher(e.DEC.CTR,e.HEAP_DATA+o,w))&&_.set(n.subarray(o,o+w),u),a+=w>>>4,u+=w,o=0,c=0;return s>0&&(c+=y(n,0,t,r,s)),this.result=_,this.counter=a,this.pos=o,this.len=c,this}AES_GCM_Decrypt_finish(){var t=this.asm,r=this.heap,s=this.tagSize,n=this.adata,h=this.counter,o=this.pos,c=this.len,u=c-s;if(c>>29,r[4]=w>>>21,r[5]=w>>>13&255,r[6]=w>>>5&255,r[7]=w<<3&255,r[8]=r[9]=r[10]=0,r[11]=y>>>29,r[12]=y>>>21&255,r[13]=y>>>13&255,r[14]=y>>>5&255,r[15]=y<<3&255,t.mac(e.MAC.GCM,e.HEAP_DATA,16),t.get_iv(e.HEAP_DATA),t.set_counter(0,0,0,this.gamma0),t.cipher(e.ENC.CTR,e.HEAP_DATA,16);var _=0;for(p=0;p16)throw new n("illegal tagSize value");this.tagSize=r}else this.tagSize=16;if(void 0===s)throw new Error("nonce is required");if(!p(s))throw new TypeError("unexpected nonce type");this.nonce=s;var f=s.length||0,l=new Uint8Array(16);12!==f?(this._gcm_mac_process(s),c[0]=c[1]=c[2]=c[3]=c[4]=c[5]=c[6]=c[7]=c[8]=c[9]=c[10]=0,c[11]=f>>>29,c[12]=f>>>21&255,c[13]=f>>>13&255,c[14]=f>>>5&255,c[15]=f<<3&255,o.mac(e.MAC.GCM,e.HEAP_DATA,16),o.get_iv(e.HEAP_DATA),o.set_iv(),l.set(c.subarray(0,16))):(l.set(s),l[15]=1);var w=new DataView(l.buffer);if(this.gamma0=w.getUint32(12),o.set_nonce(w.getUint32(0),w.getUint32(4),w.getUint32(8),0),o.set_mask(0,0,0,4294967295),void 0!==i&&null!==i){if(!p(i))throw new TypeError("unexpected adata type");if(i.length>d)throw new n("illegal adata length");i.length?(this.adata=i,this._gcm_mac_process(i)):this.adata=null}else this.adata=null;if(void 0!==a){if(!u(a))throw new TypeError("counter must be a number");if(a<1||a>4294967295)throw new RangeError("counter must be a positive 32-bit integer");this.counter=a,o.set_counter(0,0,0,this.gamma0+a|0)}else this.counter=1,o.set_counter(0,0,0,this.gamma0+1|0);if(void 0!==h){if(!u(h))throw new TypeError("iv must be a number");this.iv=h,this.AES_set_iv(h)}return this}_gcm_mac_process(t){for(var r=this.heap,s=this.asm,i=0,n=t.length||0,a=0;n>0;){for(i+=a=y(r,0,t,i,n),n-=a;15&a;)r[a++]=0;s.mac(e.MAC.GCM,e.HEAP_DATA,a)}}}A.encrypt=function(t,e,i,n,a){if(void 0===t)throw new SyntaxError("data required");if(void 0===e)throw new SyntaxError("key required");if(void 0===i)throw new SyntaxError("nonce required");return new A(e,i,n,a,r,s).encrypt(t).result},A.decrypt=function(t,e,i,n,a){if(void 0===t)throw new SyntaxError("data required");if(void 0===e)throw new SyntaxError("key required");if(void 0===i)throw new SyntaxError("nonce required");return new A(e,i,n,a,r,s).decrypt(t).result};class v{constructor(t){if(!(t=t||{}).hash)throw new SyntaxError("option 'hash' is required");if(!t.hash.HASH_SIZE)throw new SyntaxError("option 'hash' supplied doesn't seem to be a valid hash function");return this.hash=t.hash,this.BLOCK_SIZE=this.hash.BLOCK_SIZE,this.HMAC_SIZE=this.hash.HASH_SIZE,this.key=null,this.verify=null,this.result=null,void 0===t.password&&void 0===t.verify||this.reset(t),this}reset(t){var e=(t=t||{}).password;if(null===this.key&&!f(e)&&!e)throw new i("no key is associated with the instance");this.result=null,this.hash.reset(),(e||f(e))&&(this.key=x(this.hash,e));for(var r=new Uint8Array(this.key),s=0;st.BLOCK_SIZE?r.set(t.reset().process(e).finish().result):r.set(e),r}var E=64,g=32;function m(t){t=t||{},this.heap=w(Uint8Array,t.heap),this.asm=t.asm||function(t,e,r){"use asm";var s=0,i=0,n=0,a=0,h=0,o=0,c=0,u=0,f=0,l=0,p=0,w=0,y=0,_=0,d=0,A=0,v=0,x=0,E=0,g=0,m=0,b=0,S=0,C=0,M=0,U=0,H=new t.Uint8Array(r);function T(t,e,r,f,l,p,w,y,_,d,A,v,x,E,g,m){t=t|0;e=e|0;r=r|0;f=f|0;l=l|0;p=p|0;w=w|0;y=y|0;_=_|0;d=d|0;A=A|0;v=v|0;x=x|0;E=E|0;g=g|0;m=m|0;var b=0,S=0,C=0,M=0,U=0,H=0,T=0,D=0;b=s;S=i;C=n;M=a;U=h;H=o;T=c;D=u;D=t+D+(U>>>6^U>>>11^U>>>25^U<<26^U<<21^U<<7)+(T^U&(H^T))+0x428a2f98|0;M=M+D|0;D=D+(b&S^C&(b^S))+(b>>>2^b>>>13^b>>>22^b<<30^b<<19^b<<10)|0;T=e+T+(M>>>6^M>>>11^M>>>25^M<<26^M<<21^M<<7)+(H^M&(U^H))+0x71374491|0;C=C+T|0;T=T+(D&b^S&(D^b))+(D>>>2^D>>>13^D>>>22^D<<30^D<<19^D<<10)|0;H=r+H+(C>>>6^C>>>11^C>>>25^C<<26^C<<21^C<<7)+(U^C&(M^U))+0xb5c0fbcf|0;S=S+H|0;H=H+(T&D^b&(T^D))+(T>>>2^T>>>13^T>>>22^T<<30^T<<19^T<<10)|0;U=f+U+(S>>>6^S>>>11^S>>>25^S<<26^S<<21^S<<7)+(M^S&(C^M))+0xe9b5dba5|0;b=b+U|0;U=U+(H&T^D&(H^T))+(H>>>2^H>>>13^H>>>22^H<<30^H<<19^H<<10)|0;M=l+M+(b>>>6^b>>>11^b>>>25^b<<26^b<<21^b<<7)+(C^b&(S^C))+0x3956c25b|0;D=D+M|0;M=M+(U&H^T&(U^H))+(U>>>2^U>>>13^U>>>22^U<<30^U<<19^U<<10)|0;C=p+C+(D>>>6^D>>>11^D>>>25^D<<26^D<<21^D<<7)+(S^D&(b^S))+0x59f111f1|0;T=T+C|0;C=C+(M&U^H&(M^U))+(M>>>2^M>>>13^M>>>22^M<<30^M<<19^M<<10)|0;S=w+S+(T>>>6^T>>>11^T>>>25^T<<26^T<<21^T<<7)+(b^T&(D^b))+0x923f82a4|0;H=H+S|0;S=S+(C&M^U&(C^M))+(C>>>2^C>>>13^C>>>22^C<<30^C<<19^C<<10)|0;b=y+b+(H>>>6^H>>>11^H>>>25^H<<26^H<<21^H<<7)+(D^H&(T^D))+0xab1c5ed5|0;U=U+b|0;b=b+(S&C^M&(S^C))+(S>>>2^S>>>13^S>>>22^S<<30^S<<19^S<<10)|0;D=_+D+(U>>>6^U>>>11^U>>>25^U<<26^U<<21^U<<7)+(T^U&(H^T))+0xd807aa98|0;M=M+D|0;D=D+(b&S^C&(b^S))+(b>>>2^b>>>13^b>>>22^b<<30^b<<19^b<<10)|0;T=d+T+(M>>>6^M>>>11^M>>>25^M<<26^M<<21^M<<7)+(H^M&(U^H))+0x12835b01|0;C=C+T|0;T=T+(D&b^S&(D^b))+(D>>>2^D>>>13^D>>>22^D<<30^D<<19^D<<10)|0;H=A+H+(C>>>6^C>>>11^C>>>25^C<<26^C<<21^C<<7)+(U^C&(M^U))+0x243185be|0;S=S+H|0;H=H+(T&D^b&(T^D))+(T>>>2^T>>>13^T>>>22^T<<30^T<<19^T<<10)|0;U=v+U+(S>>>6^S>>>11^S>>>25^S<<26^S<<21^S<<7)+(M^S&(C^M))+0x550c7dc3|0;b=b+U|0;U=U+(H&T^D&(H^T))+(H>>>2^H>>>13^H>>>22^H<<30^H<<19^H<<10)|0;M=x+M+(b>>>6^b>>>11^b>>>25^b<<26^b<<21^b<<7)+(C^b&(S^C))+0x72be5d74|0;D=D+M|0;M=M+(U&H^T&(U^H))+(U>>>2^U>>>13^U>>>22^U<<30^U<<19^U<<10)|0;C=E+C+(D>>>6^D>>>11^D>>>25^D<<26^D<<21^D<<7)+(S^D&(b^S))+0x80deb1fe|0;T=T+C|0;C=C+(M&U^H&(M^U))+(M>>>2^M>>>13^M>>>22^M<<30^M<<19^M<<10)|0;S=g+S+(T>>>6^T>>>11^T>>>25^T<<26^T<<21^T<<7)+(b^T&(D^b))+0x9bdc06a7|0;H=H+S|0;S=S+(C&M^U&(C^M))+(C>>>2^C>>>13^C>>>22^C<<30^C<<19^C<<10)|0;b=m+b+(H>>>6^H>>>11^H>>>25^H<<26^H<<21^H<<7)+(D^H&(T^D))+0xc19bf174|0;U=U+b|0;b=b+(S&C^M&(S^C))+(S>>>2^S>>>13^S>>>22^S<<30^S<<19^S<<10)|0;t=(e>>>7^e>>>18^e>>>3^e<<25^e<<14)+(g>>>17^g>>>19^g>>>10^g<<15^g<<13)+t+d|0;D=t+D+(U>>>6^U>>>11^U>>>25^U<<26^U<<21^U<<7)+(T^U&(H^T))+0xe49b69c1|0;M=M+D|0;D=D+(b&S^C&(b^S))+(b>>>2^b>>>13^b>>>22^b<<30^b<<19^b<<10)|0;e=(r>>>7^r>>>18^r>>>3^r<<25^r<<14)+(m>>>17^m>>>19^m>>>10^m<<15^m<<13)+e+A|0;T=e+T+(M>>>6^M>>>11^M>>>25^M<<26^M<<21^M<<7)+(H^M&(U^H))+0xefbe4786|0;C=C+T|0;T=T+(D&b^S&(D^b))+(D>>>2^D>>>13^D>>>22^D<<30^D<<19^D<<10)|0;r=(f>>>7^f>>>18^f>>>3^f<<25^f<<14)+(t>>>17^t>>>19^t>>>10^t<<15^t<<13)+r+v|0;H=r+H+(C>>>6^C>>>11^C>>>25^C<<26^C<<21^C<<7)+(U^C&(M^U))+0x0fc19dc6|0;S=S+H|0;H=H+(T&D^b&(T^D))+(T>>>2^T>>>13^T>>>22^T<<30^T<<19^T<<10)|0;f=(l>>>7^l>>>18^l>>>3^l<<25^l<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+f+x|0;U=f+U+(S>>>6^S>>>11^S>>>25^S<<26^S<<21^S<<7)+(M^S&(C^M))+0x240ca1cc|0;b=b+U|0;U=U+(H&T^D&(H^T))+(H>>>2^H>>>13^H>>>22^H<<30^H<<19^H<<10)|0;l=(p>>>7^p>>>18^p>>>3^p<<25^p<<14)+(r>>>17^r>>>19^r>>>10^r<<15^r<<13)+l+E|0;M=l+M+(b>>>6^b>>>11^b>>>25^b<<26^b<<21^b<<7)+(C^b&(S^C))+0x2de92c6f|0;D=D+M|0;M=M+(U&H^T&(U^H))+(U>>>2^U>>>13^U>>>22^U<<30^U<<19^U<<10)|0;p=(w>>>7^w>>>18^w>>>3^w<<25^w<<14)+(f>>>17^f>>>19^f>>>10^f<<15^f<<13)+p+g|0;C=p+C+(D>>>6^D>>>11^D>>>25^D<<26^D<<21^D<<7)+(S^D&(b^S))+0x4a7484aa|0;T=T+C|0;C=C+(M&U^H&(M^U))+(M>>>2^M>>>13^M>>>22^M<<30^M<<19^M<<10)|0;w=(y>>>7^y>>>18^y>>>3^y<<25^y<<14)+(l>>>17^l>>>19^l>>>10^l<<15^l<<13)+w+m|0;S=w+S+(T>>>6^T>>>11^T>>>25^T<<26^T<<21^T<<7)+(b^T&(D^b))+0x5cb0a9dc|0;H=H+S|0;S=S+(C&M^U&(C^M))+(C>>>2^C>>>13^C>>>22^C<<30^C<<19^C<<10)|0;y=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(p>>>17^p>>>19^p>>>10^p<<15^p<<13)+y+t|0;b=y+b+(H>>>6^H>>>11^H>>>25^H<<26^H<<21^H<<7)+(D^H&(T^D))+0x76f988da|0;U=U+b|0;b=b+(S&C^M&(S^C))+(S>>>2^S>>>13^S>>>22^S<<30^S<<19^S<<10)|0;_=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(w>>>17^w>>>19^w>>>10^w<<15^w<<13)+_+e|0;D=_+D+(U>>>6^U>>>11^U>>>25^U<<26^U<<21^U<<7)+(T^U&(H^T))+0x983e5152|0;M=M+D|0;D=D+(b&S^C&(b^S))+(b>>>2^b>>>13^b>>>22^b<<30^b<<19^b<<10)|0;d=(A>>>7^A>>>18^A>>>3^A<<25^A<<14)+(y>>>17^y>>>19^y>>>10^y<<15^y<<13)+d+r|0;T=d+T+(M>>>6^M>>>11^M>>>25^M<<26^M<<21^M<<7)+(H^M&(U^H))+0xa831c66d|0;C=C+T|0;T=T+(D&b^S&(D^b))+(D>>>2^D>>>13^D>>>22^D<<30^D<<19^D<<10)|0;A=(v>>>7^v>>>18^v>>>3^v<<25^v<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+A+f|0;H=A+H+(C>>>6^C>>>11^C>>>25^C<<26^C<<21^C<<7)+(U^C&(M^U))+0xb00327c8|0;S=S+H|0;H=H+(T&D^b&(T^D))+(T>>>2^T>>>13^T>>>22^T<<30^T<<19^T<<10)|0;v=(x>>>7^x>>>18^x>>>3^x<<25^x<<14)+(d>>>17^d>>>19^d>>>10^d<<15^d<<13)+v+l|0;U=v+U+(S>>>6^S>>>11^S>>>25^S<<26^S<<21^S<<7)+(M^S&(C^M))+0xbf597fc7|0;b=b+U|0;U=U+(H&T^D&(H^T))+(H>>>2^H>>>13^H>>>22^H<<30^H<<19^H<<10)|0;x=(E>>>7^E>>>18^E>>>3^E<<25^E<<14)+(A>>>17^A>>>19^A>>>10^A<<15^A<<13)+x+p|0;M=x+M+(b>>>6^b>>>11^b>>>25^b<<26^b<<21^b<<7)+(C^b&(S^C))+0xc6e00bf3|0;D=D+M|0;M=M+(U&H^T&(U^H))+(U>>>2^U>>>13^U>>>22^U<<30^U<<19^U<<10)|0;E=(g>>>7^g>>>18^g>>>3^g<<25^g<<14)+(v>>>17^v>>>19^v>>>10^v<<15^v<<13)+E+w|0;C=E+C+(D>>>6^D>>>11^D>>>25^D<<26^D<<21^D<<7)+(S^D&(b^S))+0xd5a79147|0;T=T+C|0;C=C+(M&U^H&(M^U))+(M>>>2^M>>>13^M>>>22^M<<30^M<<19^M<<10)|0;g=(m>>>7^m>>>18^m>>>3^m<<25^m<<14)+(x>>>17^x>>>19^x>>>10^x<<15^x<<13)+g+y|0;S=g+S+(T>>>6^T>>>11^T>>>25^T<<26^T<<21^T<<7)+(b^T&(D^b))+0x06ca6351|0;H=H+S|0;S=S+(C&M^U&(C^M))+(C>>>2^C>>>13^C>>>22^C<<30^C<<19^C<<10)|0;m=(t>>>7^t>>>18^t>>>3^t<<25^t<<14)+(E>>>17^E>>>19^E>>>10^E<<15^E<<13)+m+_|0;b=m+b+(H>>>6^H>>>11^H>>>25^H<<26^H<<21^H<<7)+(D^H&(T^D))+0x14292967|0;U=U+b|0;b=b+(S&C^M&(S^C))+(S>>>2^S>>>13^S>>>22^S<<30^S<<19^S<<10)|0;t=(e>>>7^e>>>18^e>>>3^e<<25^e<<14)+(g>>>17^g>>>19^g>>>10^g<<15^g<<13)+t+d|0;D=t+D+(U>>>6^U>>>11^U>>>25^U<<26^U<<21^U<<7)+(T^U&(H^T))+0x27b70a85|0;M=M+D|0;D=D+(b&S^C&(b^S))+(b>>>2^b>>>13^b>>>22^b<<30^b<<19^b<<10)|0;e=(r>>>7^r>>>18^r>>>3^r<<25^r<<14)+(m>>>17^m>>>19^m>>>10^m<<15^m<<13)+e+A|0;T=e+T+(M>>>6^M>>>11^M>>>25^M<<26^M<<21^M<<7)+(H^M&(U^H))+0x2e1b2138|0;C=C+T|0;T=T+(D&b^S&(D^b))+(D>>>2^D>>>13^D>>>22^D<<30^D<<19^D<<10)|0;r=(f>>>7^f>>>18^f>>>3^f<<25^f<<14)+(t>>>17^t>>>19^t>>>10^t<<15^t<<13)+r+v|0;H=r+H+(C>>>6^C>>>11^C>>>25^C<<26^C<<21^C<<7)+(U^C&(M^U))+0x4d2c6dfc|0;S=S+H|0;H=H+(T&D^b&(T^D))+(T>>>2^T>>>13^T>>>22^T<<30^T<<19^T<<10)|0;f=(l>>>7^l>>>18^l>>>3^l<<25^l<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+f+x|0;U=f+U+(S>>>6^S>>>11^S>>>25^S<<26^S<<21^S<<7)+(M^S&(C^M))+0x53380d13|0;b=b+U|0;U=U+(H&T^D&(H^T))+(H>>>2^H>>>13^H>>>22^H<<30^H<<19^H<<10)|0;l=(p>>>7^p>>>18^p>>>3^p<<25^p<<14)+(r>>>17^r>>>19^r>>>10^r<<15^r<<13)+l+E|0;M=l+M+(b>>>6^b>>>11^b>>>25^b<<26^b<<21^b<<7)+(C^b&(S^C))+0x650a7354|0;D=D+M|0;M=M+(U&H^T&(U^H))+(U>>>2^U>>>13^U>>>22^U<<30^U<<19^U<<10)|0;p=(w>>>7^w>>>18^w>>>3^w<<25^w<<14)+(f>>>17^f>>>19^f>>>10^f<<15^f<<13)+p+g|0;C=p+C+(D>>>6^D>>>11^D>>>25^D<<26^D<<21^D<<7)+(S^D&(b^S))+0x766a0abb|0;T=T+C|0;C=C+(M&U^H&(M^U))+(M>>>2^M>>>13^M>>>22^M<<30^M<<19^M<<10)|0;w=(y>>>7^y>>>18^y>>>3^y<<25^y<<14)+(l>>>17^l>>>19^l>>>10^l<<15^l<<13)+w+m|0;S=w+S+(T>>>6^T>>>11^T>>>25^T<<26^T<<21^T<<7)+(b^T&(D^b))+0x81c2c92e|0;H=H+S|0;S=S+(C&M^U&(C^M))+(C>>>2^C>>>13^C>>>22^C<<30^C<<19^C<<10)|0;y=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(p>>>17^p>>>19^p>>>10^p<<15^p<<13)+y+t|0;b=y+b+(H>>>6^H>>>11^H>>>25^H<<26^H<<21^H<<7)+(D^H&(T^D))+0x92722c85|0;U=U+b|0;b=b+(S&C^M&(S^C))+(S>>>2^S>>>13^S>>>22^S<<30^S<<19^S<<10)|0;_=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(w>>>17^w>>>19^w>>>10^w<<15^w<<13)+_+e|0;D=_+D+(U>>>6^U>>>11^U>>>25^U<<26^U<<21^U<<7)+(T^U&(H^T))+0xa2bfe8a1|0;M=M+D|0;D=D+(b&S^C&(b^S))+(b>>>2^b>>>13^b>>>22^b<<30^b<<19^b<<10)|0;d=(A>>>7^A>>>18^A>>>3^A<<25^A<<14)+(y>>>17^y>>>19^y>>>10^y<<15^y<<13)+d+r|0;T=d+T+(M>>>6^M>>>11^M>>>25^M<<26^M<<21^M<<7)+(H^M&(U^H))+0xa81a664b|0;C=C+T|0;T=T+(D&b^S&(D^b))+(D>>>2^D>>>13^D>>>22^D<<30^D<<19^D<<10)|0;A=(v>>>7^v>>>18^v>>>3^v<<25^v<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+A+f|0;H=A+H+(C>>>6^C>>>11^C>>>25^C<<26^C<<21^C<<7)+(U^C&(M^U))+0xc24b8b70|0;S=S+H|0;H=H+(T&D^b&(T^D))+(T>>>2^T>>>13^T>>>22^T<<30^T<<19^T<<10)|0;v=(x>>>7^x>>>18^x>>>3^x<<25^x<<14)+(d>>>17^d>>>19^d>>>10^d<<15^d<<13)+v+l|0;U=v+U+(S>>>6^S>>>11^S>>>25^S<<26^S<<21^S<<7)+(M^S&(C^M))+0xc76c51a3|0;b=b+U|0;U=U+(H&T^D&(H^T))+(H>>>2^H>>>13^H>>>22^H<<30^H<<19^H<<10)|0;x=(E>>>7^E>>>18^E>>>3^E<<25^E<<14)+(A>>>17^A>>>19^A>>>10^A<<15^A<<13)+x+p|0;M=x+M+(b>>>6^b>>>11^b>>>25^b<<26^b<<21^b<<7)+(C^b&(S^C))+0xd192e819|0;D=D+M|0;M=M+(U&H^T&(U^H))+(U>>>2^U>>>13^U>>>22^U<<30^U<<19^U<<10)|0;E=(g>>>7^g>>>18^g>>>3^g<<25^g<<14)+(v>>>17^v>>>19^v>>>10^v<<15^v<<13)+E+w|0;C=E+C+(D>>>6^D>>>11^D>>>25^D<<26^D<<21^D<<7)+(S^D&(b^S))+0xd6990624|0;T=T+C|0;C=C+(M&U^H&(M^U))+(M>>>2^M>>>13^M>>>22^M<<30^M<<19^M<<10)|0;g=(m>>>7^m>>>18^m>>>3^m<<25^m<<14)+(x>>>17^x>>>19^x>>>10^x<<15^x<<13)+g+y|0;S=g+S+(T>>>6^T>>>11^T>>>25^T<<26^T<<21^T<<7)+(b^T&(D^b))+0xf40e3585|0;H=H+S|0;S=S+(C&M^U&(C^M))+(C>>>2^C>>>13^C>>>22^C<<30^C<<19^C<<10)|0;m=(t>>>7^t>>>18^t>>>3^t<<25^t<<14)+(E>>>17^E>>>19^E>>>10^E<<15^E<<13)+m+_|0;b=m+b+(H>>>6^H>>>11^H>>>25^H<<26^H<<21^H<<7)+(D^H&(T^D))+0x106aa070|0;U=U+b|0;b=b+(S&C^M&(S^C))+(S>>>2^S>>>13^S>>>22^S<<30^S<<19^S<<10)|0;t=(e>>>7^e>>>18^e>>>3^e<<25^e<<14)+(g>>>17^g>>>19^g>>>10^g<<15^g<<13)+t+d|0;D=t+D+(U>>>6^U>>>11^U>>>25^U<<26^U<<21^U<<7)+(T^U&(H^T))+0x19a4c116|0;M=M+D|0;D=D+(b&S^C&(b^S))+(b>>>2^b>>>13^b>>>22^b<<30^b<<19^b<<10)|0;e=(r>>>7^r>>>18^r>>>3^r<<25^r<<14)+(m>>>17^m>>>19^m>>>10^m<<15^m<<13)+e+A|0;T=e+T+(M>>>6^M>>>11^M>>>25^M<<26^M<<21^M<<7)+(H^M&(U^H))+0x1e376c08|0;C=C+T|0;T=T+(D&b^S&(D^b))+(D>>>2^D>>>13^D>>>22^D<<30^D<<19^D<<10)|0;r=(f>>>7^f>>>18^f>>>3^f<<25^f<<14)+(t>>>17^t>>>19^t>>>10^t<<15^t<<13)+r+v|0;H=r+H+(C>>>6^C>>>11^C>>>25^C<<26^C<<21^C<<7)+(U^C&(M^U))+0x2748774c|0;S=S+H|0;H=H+(T&D^b&(T^D))+(T>>>2^T>>>13^T>>>22^T<<30^T<<19^T<<10)|0;f=(l>>>7^l>>>18^l>>>3^l<<25^l<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+f+x|0;U=f+U+(S>>>6^S>>>11^S>>>25^S<<26^S<<21^S<<7)+(M^S&(C^M))+0x34b0bcb5|0;b=b+U|0;U=U+(H&T^D&(H^T))+(H>>>2^H>>>13^H>>>22^H<<30^H<<19^H<<10)|0;l=(p>>>7^p>>>18^p>>>3^p<<25^p<<14)+(r>>>17^r>>>19^r>>>10^r<<15^r<<13)+l+E|0;M=l+M+(b>>>6^b>>>11^b>>>25^b<<26^b<<21^b<<7)+(C^b&(S^C))+0x391c0cb3|0;D=D+M|0;M=M+(U&H^T&(U^H))+(U>>>2^U>>>13^U>>>22^U<<30^U<<19^U<<10)|0;p=(w>>>7^w>>>18^w>>>3^w<<25^w<<14)+(f>>>17^f>>>19^f>>>10^f<<15^f<<13)+p+g|0;C=p+C+(D>>>6^D>>>11^D>>>25^D<<26^D<<21^D<<7)+(S^D&(b^S))+0x4ed8aa4a|0;T=T+C|0;C=C+(M&U^H&(M^U))+(M>>>2^M>>>13^M>>>22^M<<30^M<<19^M<<10)|0;w=(y>>>7^y>>>18^y>>>3^y<<25^y<<14)+(l>>>17^l>>>19^l>>>10^l<<15^l<<13)+w+m|0;S=w+S+(T>>>6^T>>>11^T>>>25^T<<26^T<<21^T<<7)+(b^T&(D^b))+0x5b9cca4f|0;H=H+S|0;S=S+(C&M^U&(C^M))+(C>>>2^C>>>13^C>>>22^C<<30^C<<19^C<<10)|0;y=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(p>>>17^p>>>19^p>>>10^p<<15^p<<13)+y+t|0;b=y+b+(H>>>6^H>>>11^H>>>25^H<<26^H<<21^H<<7)+(D^H&(T^D))+0x682e6ff3|0;U=U+b|0;b=b+(S&C^M&(S^C))+(S>>>2^S>>>13^S>>>22^S<<30^S<<19^S<<10)|0;_=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(w>>>17^w>>>19^w>>>10^w<<15^w<<13)+_+e|0;D=_+D+(U>>>6^U>>>11^U>>>25^U<<26^U<<21^U<<7)+(T^U&(H^T))+0x748f82ee|0;M=M+D|0;D=D+(b&S^C&(b^S))+(b>>>2^b>>>13^b>>>22^b<<30^b<<19^b<<10)|0;d=(A>>>7^A>>>18^A>>>3^A<<25^A<<14)+(y>>>17^y>>>19^y>>>10^y<<15^y<<13)+d+r|0;T=d+T+(M>>>6^M>>>11^M>>>25^M<<26^M<<21^M<<7)+(H^M&(U^H))+0x78a5636f|0;C=C+T|0;T=T+(D&b^S&(D^b))+(D>>>2^D>>>13^D>>>22^D<<30^D<<19^D<<10)|0;A=(v>>>7^v>>>18^v>>>3^v<<25^v<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+A+f|0;H=A+H+(C>>>6^C>>>11^C>>>25^C<<26^C<<21^C<<7)+(U^C&(M^U))+0x84c87814|0;S=S+H|0;H=H+(T&D^b&(T^D))+(T>>>2^T>>>13^T>>>22^T<<30^T<<19^T<<10)|0;v=(x>>>7^x>>>18^x>>>3^x<<25^x<<14)+(d>>>17^d>>>19^d>>>10^d<<15^d<<13)+v+l|0;U=v+U+(S>>>6^S>>>11^S>>>25^S<<26^S<<21^S<<7)+(M^S&(C^M))+0x8cc70208|0;b=b+U|0;U=U+(H&T^D&(H^T))+(H>>>2^H>>>13^H>>>22^H<<30^H<<19^H<<10)|0;x=(E>>>7^E>>>18^E>>>3^E<<25^E<<14)+(A>>>17^A>>>19^A>>>10^A<<15^A<<13)+x+p|0;M=x+M+(b>>>6^b>>>11^b>>>25^b<<26^b<<21^b<<7)+(C^b&(S^C))+0x90befffa|0;D=D+M|0;M=M+(U&H^T&(U^H))+(U>>>2^U>>>13^U>>>22^U<<30^U<<19^U<<10)|0;E=(g>>>7^g>>>18^g>>>3^g<<25^g<<14)+(v>>>17^v>>>19^v>>>10^v<<15^v<<13)+E+w|0;C=E+C+(D>>>6^D>>>11^D>>>25^D<<26^D<<21^D<<7)+(S^D&(b^S))+0xa4506ceb|0;T=T+C|0;C=C+(M&U^H&(M^U))+(M>>>2^M>>>13^M>>>22^M<<30^M<<19^M<<10)|0;g=(m>>>7^m>>>18^m>>>3^m<<25^m<<14)+(x>>>17^x>>>19^x>>>10^x<<15^x<<13)+g+y|0;S=g+S+(T>>>6^T>>>11^T>>>25^T<<26^T<<21^T<<7)+(b^T&(D^b))+0xbef9a3f7|0;H=H+S|0;S=S+(C&M^U&(C^M))+(C>>>2^C>>>13^C>>>22^C<<30^C<<19^C<<10)|0;m=(t>>>7^t>>>18^t>>>3^t<<25^t<<14)+(E>>>17^E>>>19^E>>>10^E<<15^E<<13)+m+_|0;b=m+b+(H>>>6^H>>>11^H>>>25^H<<26^H<<21^H<<7)+(D^H&(T^D))+0xc67178f2|0;U=U+b|0;b=b+(S&C^M&(S^C))+(S>>>2^S>>>13^S>>>22^S<<30^S<<19^S<<10)|0;s=s+b|0;i=i+S|0;n=n+C|0;a=a+M|0;h=h+U|0;o=o+H|0;c=c+T|0;u=u+D|0}function D(t){t=t|0;T(H[t|0]<<24|H[t|1]<<16|H[t|2]<<8|H[t|3],H[t|4]<<24|H[t|5]<<16|H[t|6]<<8|H[t|7],H[t|8]<<24|H[t|9]<<16|H[t|10]<<8|H[t|11],H[t|12]<<24|H[t|13]<<16|H[t|14]<<8|H[t|15],H[t|16]<<24|H[t|17]<<16|H[t|18]<<8|H[t|19],H[t|20]<<24|H[t|21]<<16|H[t|22]<<8|H[t|23],H[t|24]<<24|H[t|25]<<16|H[t|26]<<8|H[t|27],H[t|28]<<24|H[t|29]<<16|H[t|30]<<8|H[t|31],H[t|32]<<24|H[t|33]<<16|H[t|34]<<8|H[t|35],H[t|36]<<24|H[t|37]<<16|H[t|38]<<8|H[t|39],H[t|40]<<24|H[t|41]<<16|H[t|42]<<8|H[t|43],H[t|44]<<24|H[t|45]<<16|H[t|46]<<8|H[t|47],H[t|48]<<24|H[t|49]<<16|H[t|50]<<8|H[t|51],H[t|52]<<24|H[t|53]<<16|H[t|54]<<8|H[t|55],H[t|56]<<24|H[t|57]<<16|H[t|58]<<8|H[t|59],H[t|60]<<24|H[t|61]<<16|H[t|62]<<8|H[t|63])}function k(t){t=t|0;H[t|0]=s>>>24;H[t|1]=s>>>16&255;H[t|2]=s>>>8&255;H[t|3]=s&255;H[t|4]=i>>>24;H[t|5]=i>>>16&255;H[t|6]=i>>>8&255;H[t|7]=i&255;H[t|8]=n>>>24;H[t|9]=n>>>16&255;H[t|10]=n>>>8&255;H[t|11]=n&255;H[t|12]=a>>>24;H[t|13]=a>>>16&255;H[t|14]=a>>>8&255;H[t|15]=a&255;H[t|16]=h>>>24;H[t|17]=h>>>16&255;H[t|18]=h>>>8&255;H[t|19]=h&255;H[t|20]=o>>>24;H[t|21]=o>>>16&255;H[t|22]=o>>>8&255;H[t|23]=o&255;H[t|24]=c>>>24;H[t|25]=c>>>16&255;H[t|26]=c>>>8&255;H[t|27]=c&255;H[t|28]=u>>>24;H[t|29]=u>>>16&255;H[t|30]=u>>>8&255;H[t|31]=u&255}function G(){s=0x6a09e667;i=0xbb67ae85;n=0x3c6ef372;a=0xa54ff53a;h=0x510e527f;o=0x9b05688c;c=0x1f83d9ab;u=0x5be0cd19;f=l=0}function I(t,e,r,p,w,y,_,d,A,v){t=t|0;e=e|0;r=r|0;p=p|0;w=w|0;y=y|0;_=_|0;d=d|0;A=A|0;v=v|0;s=t;i=e;n=r;a=p;h=w;o=y;c=_;u=d;f=A;l=v}function Z(t,e){t=t|0;e=e|0;var r=0;if(t&63)return-1;while((e|0)>=64){D(t);t=t+64|0;e=e-64|0;r=r+64|0}f=f+r|0;if(f>>>0>>0)l=l+1|0;return r|0}function P(t,e,r){t=t|0;e=e|0;r=r|0;var s=0,i=0;if(t&63)return-1;if(~r)if(r&31)return-1;if((e|0)>=64){s=Z(t,e)|0;if((s|0)==-1)return-1;t=t+s|0;e=e-s|0}s=s+e|0;f=f+e|0;if(f>>>0>>0)l=l+1|0;H[t|e]=0x80;if((e|0)>=56){for(i=e+1|0;(i|0)<64;i=i+1|0)H[t|i]=0x00;D(t);e=0;H[t|0]=0}for(i=e+1|0;(i|0)<59;i=i+1|0)H[t|i]=0;H[t|56]=l>>>21&255;H[t|57]=l>>>13&255;H[t|58]=l>>>5&255;H[t|59]=l<<3&255|f>>>29;H[t|60]=f>>>21&255;H[t|61]=f>>>13&255;H[t|62]=f>>>5&255;H[t|63]=f<<3&255;D(t);if(~r)k(r);return s|0}function B(){s=p;i=w;n=y;a=_;h=d;o=A;c=v;u=x;f=64;l=0}function z(){s=E;i=g;n=m;a=b;h=S;o=C;c=M;u=U;f=64;l=0}function O(t,e,r,H,D,k,I,Z,P,B,z,O,q,L,R,K){t=t|0;e=e|0;r=r|0;H=H|0;D=D|0;k=k|0;I=I|0;Z=Z|0;P=P|0;B=B|0;z=z|0;O=O|0;q=q|0;L=L|0;R=R|0;K=K|0;G();T(t^0x5c5c5c5c,e^0x5c5c5c5c,r^0x5c5c5c5c,H^0x5c5c5c5c,D^0x5c5c5c5c,k^0x5c5c5c5c,I^0x5c5c5c5c,Z^0x5c5c5c5c,P^0x5c5c5c5c,B^0x5c5c5c5c,z^0x5c5c5c5c,O^0x5c5c5c5c,q^0x5c5c5c5c,L^0x5c5c5c5c,R^0x5c5c5c5c,K^0x5c5c5c5c);E=s;g=i;m=n;b=a;S=h;C=o;M=c;U=u;G();T(t^0x36363636,e^0x36363636,r^0x36363636,H^0x36363636,D^0x36363636,k^0x36363636,I^0x36363636,Z^0x36363636,P^0x36363636,B^0x36363636,z^0x36363636,O^0x36363636,q^0x36363636,L^0x36363636,R^0x36363636,K^0x36363636);p=s;w=i;y=n;_=a;d=h;A=o;v=c;x=u;f=64;l=0}function q(t,e,r){t=t|0;e=e|0;r=r|0;var f=0,l=0,p=0,w=0,y=0,_=0,d=0,A=0,v=0;if(t&63)return-1;if(~r)if(r&31)return-1;v=P(t,e,-1)|0;f=s,l=i,p=n,w=a,y=h,_=o,d=c,A=u;z();T(f,l,p,w,y,_,d,A,0x80000000,0,0,0,0,0,0,768);if(~r)k(r);return v|0}function L(t,e,r,f,l){t=t|0;e=e|0;r=r|0;f=f|0;l=l|0;var p=0,w=0,y=0,_=0,d=0,A=0,v=0,x=0,E=0,g=0,m=0,b=0,S=0,C=0,M=0,U=0;if(t&63)return-1;if(~l)if(l&31)return-1;H[t+e|0]=r>>>24;H[t+e+1|0]=r>>>16&255;H[t+e+2|0]=r>>>8&255;H[t+e+3|0]=r&255;q(t,e+4|0,-1)|0;p=E=s,w=g=i,y=m=n,_=b=a,d=S=h,A=C=o,v=M=c,x=U=u;f=f-1|0;while((f|0)>0){B();T(E,g,m,b,S,C,M,U,0x80000000,0,0,0,0,0,0,768);E=s,g=i,m=n,b=a,S=h,C=o,M=c,U=u;z();T(E,g,m,b,S,C,M,U,0x80000000,0,0,0,0,0,0,768);E=s,g=i,m=n,b=a,S=h,C=o,M=c,U=u;p=p^s;w=w^i;y=y^n;_=_^a;d=d^h;A=A^o;v=v^c;x=x^u;f=f-1|0}s=p;i=w;n=y;a=_;h=d;o=A;c=v;u=x;if(~l)k(l);return 0}return{reset:G,init:I,process:Z,finish:P,hmac_reset:B,hmac_init:O,hmac_finish:q,pbkdf2_generate_block:L}}({Uint8Array:Uint8Array},null,this.heap.buffer),this.BLOCK_SIZE=E,this.HASH_SIZE=g,this.reset()}m.BLOCK_SIZE=E,m.HASH_SIZE=g,m.NAME="sha256";var b=m.prototype;b.reset=function(){return this.result=null,this.pos=0,this.len=0,this.asm.reset(),this},b.process=function(t){if(null!==this.result)throw new i("state must be reset before processing new data");if(f(t)&&(t=h(t)),l(t)&&(t=new Uint8Array(t)),!p(t))throw new TypeError("data isn't of expected type");for(var e=this.asm,r=this.heap,s=this.pos,n=this.len,a=0,o=t.length,c=0;o>0;)n+=c=y(r,s+n,t,a,o),a+=c,o-=c,s+=c=e.process(s,n),(n-=c)||(s=0);return this.pos=s,this.len=n,this},b.finish=function(){if(null!==this.result)throw new i("state must be reset before processing new data");return this.asm.finish(this.pos,this.len,0),this.result=new Uint8Array(this.HASH_SIZE),this.result.set(this.heap.subarray(0,this.HASH_SIZE)),this.pos=0,this.len=0,this};var S=null;class C extends v{constructor(t){(t=t||{}).hash instanceof m||(t.hash=(null===S&&(S=new m({heapSize:1048576})),S)),super(t)}reset(t){t=t||{},this.result=null,this.hash.reset();var e=t.password;if(void 0!==e){f(e)&&(e=h(e));var r=this.key=x(this.hash,e);this.hash.reset().asm.hmac_init(r[0]<<24|r[1]<<16|r[2]<<8|r[3],r[4]<<24|r[5]<<16|r[6]<<8|r[7],r[8]<<24|r[9]<<16|r[10]<<8|r[11],r[12]<<24|r[13]<<16|r[14]<<8|r[15],r[16]<<24|r[17]<<16|r[18]<<8|r[19],r[20]<<24|r[21]<<16|r[22]<<8|r[23],r[24]<<24|r[25]<<16|r[26]<<8|r[27],r[28]<<24|r[29]<<16|r[30]<<8|r[31],r[32]<<24|r[33]<<16|r[34]<<8|r[35],r[36]<<24|r[37]<<16|r[38]<<8|r[39],r[40]<<24|r[41]<<16|r[42]<<8|r[43],r[44]<<24|r[45]<<16|r[46]<<8|r[47],r[48]<<24|r[49]<<16|r[50]<<8|r[51],r[52]<<24|r[53]<<16|r[54]<<8|r[55],r[56]<<24|r[57]<<16|r[58]<<8|r[59],r[60]<<24|r[61]<<16|r[62]<<8|r[63])}else this.hash.asm.hmac_reset();var s=t.verify;return void 0!==s?this._hmac_init_verify(s):this.verify=null,this}finish(){if(null===this.key)throw new i("no key is associated with the instance");if(null!==this.result)throw new i("state must be reset before processing new data");var t=this.hash,e=this.hash.asm,r=this.hash.heap;e.hmac_finish(t.pos,t.len,0);var s=this.verify,n=new Uint8Array(g);if(n.set(r.subarray(0,g)),s)if(s.length===n.length){for(var a=0,h=0;h>>24&255,a>>>16&255,a>>>8&255,255&a])).finish().result);this.result.set(c.subarray(0,o),h);for(var u=1;u
+
\ No newline at end of file
diff --git a/assets/error.svg b/assets/error.svg
new file mode 100644
index 00000000..0d478816
--- /dev/null
+++ b/assets/error.svg
@@ -0,0 +1,258 @@
+
+
diff --git a/assets/eye-off.svg b/assets/eye-off.svg
new file mode 100644
index 00000000..6edb1745
--- /dev/null
+++ b/assets/eye-off.svg
@@ -0,0 +1 @@
+
diff --git a/assets/eye.svg b/assets/eye.svg
new file mode 100644
index 00000000..059dd2e9
--- /dev/null
+++ b/assets/eye.svg
@@ -0,0 +1 @@
+
diff --git a/assets/favicon-120.png b/assets/favicon-120.png
deleted file mode 100644
index 1fa60cd8..00000000
Binary files a/assets/favicon-120.png and /dev/null differ
diff --git a/assets/favicon-128.png b/assets/favicon-128.png
deleted file mode 100644
index 45f37fce..00000000
Binary files a/assets/favicon-128.png and /dev/null differ
diff --git a/assets/favicon-144.png b/assets/favicon-144.png
deleted file mode 100644
index 5f822567..00000000
Binary files a/assets/favicon-144.png and /dev/null differ
diff --git a/assets/favicon-152.png b/assets/favicon-152.png
deleted file mode 100644
index afa2e978..00000000
Binary files a/assets/favicon-152.png and /dev/null differ
diff --git a/assets/favicon-167.png b/assets/favicon-167.png
deleted file mode 100644
index d39f8a0c..00000000
Binary files a/assets/favicon-167.png and /dev/null differ
diff --git a/assets/favicon-16x16.png b/assets/favicon-16x16.png
new file mode 100644
index 00000000..1926e1bf
Binary files /dev/null and b/assets/favicon-16x16.png differ
diff --git a/assets/favicon-180.png b/assets/favicon-180.png
deleted file mode 100644
index 67a23392..00000000
Binary files a/assets/favicon-180.png and /dev/null differ
diff --git a/assets/favicon-195.png b/assets/favicon-195.png
deleted file mode 100644
index bb763ea5..00000000
Binary files a/assets/favicon-195.png and /dev/null differ
diff --git a/assets/favicon-196.png b/assets/favicon-196.png
deleted file mode 100644
index 8ca0f0b9..00000000
Binary files a/assets/favicon-196.png and /dev/null differ
diff --git a/assets/favicon-228.png b/assets/favicon-228.png
deleted file mode 100644
index 3df9e05c..00000000
Binary files a/assets/favicon-228.png and /dev/null differ
diff --git a/assets/favicon-32.png b/assets/favicon-32.png
deleted file mode 100644
index 045edd37..00000000
Binary files a/assets/favicon-32.png and /dev/null differ
diff --git a/assets/favicon-32x32.png b/assets/favicon-32x32.png
new file mode 100644
index 00000000..f00f4ee7
Binary files /dev/null and b/assets/favicon-32x32.png differ
diff --git a/assets/favicon-96.png b/assets/favicon-96.png
deleted file mode 100644
index ab851902..00000000
Binary files a/assets/favicon-96.png and /dev/null differ
diff --git a/assets/github-icon.svg b/assets/github-icon.svg
deleted file mode 100644
index 44e074c8..00000000
--- a/assets/github-icon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/icon-64x64.png b/assets/icon-64x64.png
new file mode 100644
index 00000000..e3a6b2a7
Binary files /dev/null and b/assets/icon-64x64.png differ
diff --git a/assets/icon.svg b/assets/icon.svg
new file mode 100644
index 00000000..e4bfb3d1
--- /dev/null
+++ b/assets/icon.svg
@@ -0,0 +1,34 @@
+
+
diff --git a/assets/illustration_download.svg b/assets/illustration_download.svg
deleted file mode 100644
index 9425fc28..00000000
--- a/assets/illustration_download.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/illustration_error.svg b/assets/illustration_error.svg
deleted file mode 100644
index cba8d2e0..00000000
--- a/assets/illustration_error.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/illustration_expired.svg b/assets/illustration_expired.svg
deleted file mode 100644
index 1d569f08..00000000
--- a/assets/illustration_expired.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/lock.svg b/assets/lock.svg
new file mode 100644
index 00000000..d4e16c96
--- /dev/null
+++ b/assets/lock.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/assets/master-logo.svg b/assets/master-logo.svg
new file mode 100644
index 00000000..ec8e6f8f
--- /dev/null
+++ b/assets/master-logo.svg
@@ -0,0 +1,99 @@
+
diff --git a/assets/mozilla-logo.svg b/assets/mozilla-logo.svg
deleted file mode 100644
index 3ea2e868..00000000
--- a/assets/mozilla-logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/notFound.svg b/assets/notFound.svg
new file mode 100644
index 00000000..24e04000
--- /dev/null
+++ b/assets/notFound.svg
@@ -0,0 +1,274 @@
+
+
diff --git a/assets/safari-pinned-tab.svg b/assets/safari-pinned-tab.svg
new file mode 100644
index 00000000..d1fec797
--- /dev/null
+++ b/assets/safari-pinned-tab.svg
@@ -0,0 +1,34 @@
+
+
diff --git a/assets/select-arrow.svg b/assets/select-arrow.svg
new file mode 100644
index 00000000..d43db4d0
--- /dev/null
+++ b/assets/select-arrow.svg
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file
diff --git a/assets/send-fb.jpg b/assets/send-fb.jpg
index 784fe51f..a9dcbdbf 100644
Binary files a/assets/send-fb.jpg and b/assets/send-fb.jpg differ
diff --git a/assets/send-twitter.jpg b/assets/send-twitter.jpg
index d13d5f90..ae30ad54 100644
Binary files a/assets/send-twitter.jpg and b/assets/send-twitter.jpg differ
diff --git a/assets/send_bg.svg b/assets/send_bg.svg
deleted file mode 100644
index a4557acc..00000000
--- a/assets/send_bg.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/send_logo.svg b/assets/send_logo.svg
deleted file mode 100644
index b13d138f..00000000
--- a/assets/send_logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/share-24.svg b/assets/share-24.svg
new file mode 100644
index 00000000..f85064c9
--- /dev/null
+++ b/assets/share-24.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/assets/spinner.svg b/assets/spinner.svg
deleted file mode 100644
index 004a3566..00000000
--- a/assets/spinner.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
\ No newline at end of file
diff --git a/assets/thunderbird-icon.svg b/assets/thunderbird-icon.svg
new file mode 100644
index 00000000..4f555086
--- /dev/null
+++ b/assets/thunderbird-icon.svg
@@ -0,0 +1,65 @@
+
\ No newline at end of file
diff --git a/assets/twitter-icon.svg b/assets/twitter-icon.svg
deleted file mode 100644
index 8816009a..00000000
--- a/assets/twitter-icon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/upload.svg b/assets/upload.svg
deleted file mode 100644
index f98e6b2e..00000000
--- a/assets/upload.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/user.svg b/assets/user.svg
new file mode 100644
index 00000000..3d052d07
--- /dev/null
+++ b/assets/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/wordmark.svg b/assets/wordmark.svg
new file mode 100644
index 00000000..bc4841c6
--- /dev/null
+++ b/assets/wordmark.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets_src/apple-touch-icon.xcf b/assets_src/apple-touch-icon.xcf
new file mode 100644
index 00000000..71584e92
Binary files /dev/null and b/assets_src/apple-touch-icon.xcf differ
diff --git a/assets_src/completed.svg b/assets_src/completed.svg
new file mode 100644
index 00000000..7d4736cb
--- /dev/null
+++ b/assets_src/completed.svg
@@ -0,0 +1,284 @@
+
+
diff --git a/assets_src/error.svg b/assets_src/error.svg
new file mode 100644
index 00000000..cea8a7ac
--- /dev/null
+++ b/assets_src/error.svg
@@ -0,0 +1,284 @@
+
+
diff --git a/assets_src/icon.svg b/assets_src/icon.svg
new file mode 100644
index 00000000..efa7b48e
--- /dev/null
+++ b/assets_src/icon.svg
@@ -0,0 +1,76 @@
+
+
diff --git a/assets_src/notFound.svg b/assets_src/notFound.svg
new file mode 100644
index 00000000..ce74ddb8
--- /dev/null
+++ b/assets_src/notFound.svg
@@ -0,0 +1,300 @@
+
+
diff --git a/assets_src/safari-pinned-tab.svg b/assets_src/safari-pinned-tab.svg
new file mode 100644
index 00000000..4f7a6f54
--- /dev/null
+++ b/assets_src/safari-pinned-tab.svg
@@ -0,0 +1,76 @@
+
+
diff --git a/assets_src/send-header.xcf b/assets_src/send-header.xcf
new file mode 100644
index 00000000..bff45898
Binary files /dev/null and b/assets_src/send-header.xcf differ
diff --git a/browserconfig.xml b/browserconfig.xml
deleted file mode 100644
index a1ff7e28..00000000
--- a/browserconfig.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
- #0297F8
-
-
-
\ No newline at end of file
diff --git a/browserslist b/browserslist
index f8713fdd..6ba761ca 100644
--- a/browserslist
+++ b/browserslist
@@ -2,4 +2,5 @@ last 2 chrome versions
last 2 firefox versions
last 2 safari versions
last 2 edge versions
+edge 18
firefox esr
diff --git a/build/android_index_plugin.js b/build/android_index_plugin.js
new file mode 100644
index 00000000..7f37d608
--- /dev/null
+++ b/build/android_index_plugin.js
@@ -0,0 +1,50 @@
+const path = require('path');
+const html = require('choo/html');
+const NAME = 'AndroidIndexPlugin';
+
+function chunkFileNames(compilation) {
+ const names = {};
+ for (const chunk of compilation.chunks) {
+ for (const file of chunk.files) {
+ if (!/\.map$/.test(file)) {
+ names[`${chunk.name}${path.extname(file)}`] = file;
+ }
+ }
+ }
+ return names;
+}
+class AndroidIndexPlugin {
+ apply(compiler) {
+ compiler.hooks.emit.tap(NAME, compilation => {
+ const files = chunkFileNames(compilation);
+ const page = html`
+
+
+ Send
+
+
+
+
+
+
+
+
+ `
+ .toString()
+ .replace(/\n\s{6}/g, '\n');
+ compilation.assets['android.html'] = {
+ source() {
+ return page;
+ },
+ size() {
+ return page.length;
+ }
+ };
+ });
+ }
+}
+
+module.exports = AndroidIndexPlugin;
diff --git a/build/fluent_loader.js b/build/fluent_loader.js
deleted file mode 100644
index b5e4f392..00000000
--- a/build/fluent_loader.js
+++ /dev/null
@@ -1,63 +0,0 @@
-// TODO: when node supports 'for await' we can remove babel-polyfill
-// and use 'fluent' instead of 'fluent/compat' (also below near line 42)
-require('babel-polyfill');
-const { MessageContext } = require('fluent/compat');
-const fs = require('fs');
-
-function toJSON(map) {
- return JSON.stringify(Array.from(map));
-}
-
-function merge(m1, m2) {
- const result = new Map(m1);
- for (const [k, v] of m2) {
- result.set(k, v);
- }
- return result;
-}
-
-module.exports = function(source) {
- const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
- const result = localeExp.exec(this.resourcePath);
- const locale = result && result[1];
- if (!locale) {
- throw new Error(`couldn't find locale in: ${this.resourcePath}`);
- }
- // load default language and "merge" contexts
- // TODO: make this configurable
- const en_ftl = fs.readFileSync(
- require.resolve('../public/locales/en-US/send.ftl'),
- 'utf8'
- );
- const en = new MessageContext('en-US');
- en.addMessages(en_ftl);
- // pre-parse the ftl
- const context = new MessageContext(locale);
- context.addMessages(source);
-
- const merged = merge(en._messages, context._messages);
- return `
-module.exports = \`
-if (typeof window === 'undefined') {
- require('babel-polyfill');
- var fluent = require('fluent/compat');
-}
-(function () {
- var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
- ctx._messages = new Map(${toJSON(merged)});
- function translate(id, data) {
- var msg = ctx.getMessage(id);
- if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
- msg = msg.attrs.title || msg.attrs.alt
- }
- return ctx.format(msg, data);
- }
- if (typeof window === 'undefined') {
- module.exports = translate;
- }
- else {
- window.translate = translate;
- }
-})();
-\``;
-};
diff --git a/build/generate_l10n_map.js b/build/generate_l10n_map.js
deleted file mode 100644
index ed33c0f8..00000000
--- a/build/generate_l10n_map.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- This code is included by both the server and frontend via
- common/locales.js
-
- When included from the server the export will be the function.
-
- When included from the frontend (via webpack) the export will
- be an object mapping ftl files to js files. Example:
- "public/locales/en-US/send.ftl":"public/locales/en-US/send.6b4f8354.js"
-*/
-
-const fs = require('fs');
-const path = require('path');
-
-function kv(d) {
- return `"${d}": require('../public/locales/${d}/send.ftl')`;
-}
-
-module.exports = function() {
- const dirs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
- const code = `
- module.exports = {
- translate: function (id, data) { return window.translate(id, data) },
- ${dirs.map(kv).join(',\n')}
- };`;
- return {
- code,
- dependencies: dirs.map(d =>
- require.resolve(`../public/locales/${d}/send.ftl`)
- ),
- cacheable: true
- };
-};
diff --git a/build/package_json_loader.js b/build/package_json_loader.js
deleted file mode 100644
index a03678f5..00000000
--- a/build/package_json_loader.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const commit = require('git-rev-sync').short();
-
-module.exports = function(source) {
- const pkg = JSON.parse(source);
- const version = {
- commit,
- source: pkg.homepage,
- version: process.env.CIRCLE_TAG || `v${pkg.version}`
- };
- return `module.exports = '${JSON.stringify(version)}'`;
-};
diff --git a/build/readme.md b/build/readme.md
index ff0f9c5d..b8fc18d7 100644
--- a/build/readme.md
+++ b/build/readme.md
@@ -1,24 +1,12 @@
# Custom Loaders
-## Fluent Loader
+## Android Index Plugin
-The fluent loader "compiles" `.ftl` files into `.js` files directly usable by both the frontend and server for localization.
+Generates the `index.html` page for the native android client
-## Generate Asset Map
+## Version Plugin
-This loader enumerates all the files in `assets/` so that `common/assets.js` can provide mappings from the source filename to the hashed filename used on the site.
-
-## Generate L10N Map
-
-This loader enumerates all the ftl files in `public/locales` so that the fluent loader can create it's js files.
-
-## Package.json Loader
-
-This loader creates a `version.json` file that gets exposed by the `/__version__` route from the `package.json` file and current git commit hash.
-
-## Version Loader
-
-This loader substitutes the string "VERSION" for the version string specified in `package.json`. This is a workaround because `package.json` already uses the `package_json_loader`. See [app/templates/header/index.js](../app/templates/header/index.js) for more info.
+Creates a `version.json` file that gets exposed by the `/__version__` route from the `package.json` file and current git commit hash.
# See Also
diff --git a/build/version_loader.js b/build/version_loader.js
deleted file mode 100644
index 78a2c019..00000000
--- a/build/version_loader.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const version = require('../package.json').version;
-
-module.exports = function(source) {
- return source.replace('VERSION', version);
-};
diff --git a/build/version_plugin.js b/build/version_plugin.js
new file mode 100644
index 00000000..af351a3a
--- /dev/null
+++ b/build/version_plugin.js
@@ -0,0 +1,33 @@
+const gitRevSync = require('git-rev-sync');
+const pkg = require('../package.json');
+
+let commit = 'unknown';
+
+try {
+ commit = gitRevSync.short();
+} catch (e) {
+ console.warn('Error fetching current git commit: ' + e);
+}
+
+const version = JSON.stringify({
+ commit,
+ source: pkg.homepage,
+ version: process.env.CIRCLE_TAG || `v${pkg.version}`
+});
+
+class VersionPlugin {
+ apply(compiler) {
+ compiler.hooks.emit.tap('VersionPlugin', compilation => {
+ compilation.assets['version.json'] = {
+ source() {
+ return version;
+ },
+ size() {
+ return version.length;
+ }
+ };
+ });
+ }
+}
+
+module.exports = VersionPlugin;
diff --git a/common/assets.js b/common/assets.js
index b1db9acf..f1a4657e 100644
--- a/common/assets.js
+++ b/common/assets.js
@@ -1,6 +1,6 @@
-const genmap = require('../build/generate_asset_map');
+const genmap = require('./generate_asset_map');
const isServer = typeof genmap === 'function';
-const prefix = isServer ? '/' : '';
+let prefix = '';
let manifest = {};
try {
//eslint-disable-next-line node/no-missing-require
@@ -15,15 +15,38 @@ function getAsset(name) {
return prefix + assets[name];
}
+function setPrefix(name) {
+ prefix = name;
+}
+
+function getMatches(match) {
+ return Object.keys(assets)
+ .filter(k => match.test(k))
+ .map(getAsset);
+}
+
const instance = {
+ setPrefix: setPrefix,
get: getAsset,
+ match: getMatches,
setMiddleware: function(middleware) {
+ function getManifest() {
+ return JSON.parse(
+ middleware.fileSystem.readFileSync(
+ middleware.getFilenameFromUrl('/manifest.json')
+ )
+ );
+ }
if (middleware) {
instance.get = function getAssetWithMiddleware(name) {
- const f = middleware.fileSystem.readFileSync(
- middleware.getFilenameFromUrl('/manifest.json')
- );
- return prefix + JSON.parse(f)[name];
+ const m = getManifest();
+ return prefix + m[name];
+ };
+ instance.match = function matchAssetWithMiddleware(match) {
+ const m = getManifest();
+ return Object.keys(m)
+ .filter(k => match.test(k))
+ .map(k => prefix + m[k]);
};
}
}
diff --git a/build/generate_asset_map.js b/common/generate_asset_map.js
similarity index 93%
rename from build/generate_asset_map.js
rename to common/generate_asset_map.js
index de0999c9..6289e54c 100644
--- a/build/generate_asset_map.js
+++ b/common/generate_asset_map.js
@@ -19,7 +19,6 @@ function kv(f) {
module.exports = function() {
const files = fs.readdirSync(path.join(__dirname, '..', 'assets'));
const code = `module.exports = {
- "package.json": require('../package.json'),
${files.map(kv).join(',\n')}
};`;
return {
diff --git a/common/locales.js b/common/locales.js
deleted file mode 100644
index ba1476de..00000000
--- a/common/locales.js
+++ /dev/null
@@ -1,52 +0,0 @@
-const gen = require('../build/generate_l10n_map');
-
-const isServer = typeof gen === 'function';
-const prefix = isServer ? '/' : '';
-let manifest = {};
-try {
- // eslint-disable-next-line node/no-missing-require
- manifest = require('../dist/manifest.json');
-} catch (e) {
- // use middleware
-}
-
-const locales = isServer ? manifest : gen;
-
-function getLocale(name) {
- return prefix + locales[`public/locales/${name}/send.ftl`];
-}
-
-function serverTranslator(name) {
- // eslint-disable-next-line security/detect-non-literal-require
- return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
-}
-
-function browserTranslator() {
- return locales.translate;
-}
-
-const translator = isServer ? serverTranslator : browserTranslator;
-
-const instance = {
- get: getLocale,
- getTranslator: translator,
- setMiddleware: function(middleware) {
- if (middleware) {
- const _eval = require('require-from-string');
- instance.get = function getLocaleWithMiddleware(name) {
- const f = middleware.fileSystem.readFileSync(
- middleware.getFilenameFromUrl('/manifest.json')
- );
- return prefix + JSON.parse(f)[`public/locales/${name}/send.ftl`];
- };
- instance.getTranslator = function(name) {
- const f = middleware.fileSystem.readFileSync(
- middleware.getFilenameFromUrl(instance.get(name))
- );
- return _eval(f.toString());
- };
- }
- }
-};
-
-module.exports = instance;
diff --git a/common/readme.md b/common/readme.md
index 9e351eee..0aaad7ac 100644
--- a/common/readme.md
+++ b/common/readme.md
@@ -1,3 +1,7 @@
# Common Code
-This directory contains code loaded by both the frontend `app` and backend `server`. The code here can be challenging to understand at first because the contexts for the two (three counting the dev server) environments that include them are quite different, but the purpose of these modules are quite simple, to provide mappings from the source assets (`copy-16.png`) to the concrete production assets (`copy-16.db66e0bf.svg`), similarly for localizations.
\ No newline at end of file
+This directory contains code loaded by both the frontend `app` and backend `server`. The code here can be challenging to understand at first because the contexts for the two (three counting the dev server) environments that include them are quite different, but the purpose of these modules are quite simple, to provide mappings from the source assets (`copy-16.png`) to the concrete production assets (`copy-16.db66e0bf.svg`).
+
+## Generate Asset Map
+
+This loader enumerates all the files in `assets/` so that `common/assets.js` can provide mappings from the source filename to the hashed filename used on the site.
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index f72bf161..8ad9fc6f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,3 +10,10 @@ services:
- REDIS_HOST=redis
redis:
image: redis:alpine
+ selenium-firefox:
+ image: b4handjr/selenium-firefox
+ ports:
+ - "${VNC_PORT:-5900}:5900"
+ shm_size: 2g
+ volumes:
+ - .:/code
diff --git a/docs/AWS.md b/docs/AWS.md
new file mode 100644
index 00000000..999dba85
--- /dev/null
+++ b/docs/AWS.md
@@ -0,0 +1,236 @@
+# Deployment to AWS
+
+This document describes how to do a deployment of Send in AWS
+
+## AWS requirements
+
+### Security groups (2)
+
+* ALB:
+ - inbound: allow traffic from anywhere on port 80 and 443
+ - ountbound: allow traffic to the instance security group on port `8080`
+
+* Instance:
+ - inbound: allow SSH from your public IP or a bastion (changing the default SSH port is a good idea)
+ - inbound: allow traffic from the ALB security group on port `8080`
+ - ountbound: allow all traffic to anywhere
+
+### Resources
+
+* An S3 bucket (block all public access)
+
+* A private EC2 instance running Ubuntu `20.04` (you can use the [Amazon EC2 AMI Locator](https://cloud-images.ubuntu.com/locator/ec2/) to find the latest)
+
+ Attach an IAM role to the instance with the following inline policy:
+
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Action": [
+ "s3:ListAllMyBuckets"
+ ],
+ "Resource": [
+ "*"
+ ],
+ "Effect": "Allow"
+ },
+ {
+ "Action": [
+ "s3:ListBucket",
+ "s3:GetBucketLocation",
+ "s3:ListBucketMultipartUploads"
+ ],
+ "Resource": [
+ "arn:aws:s3:::"
+ ],
+ "Effect": "Allow"
+ },
+ {
+ "Action": [
+ "s3:GetObject",
+ "s3:GetObjectVersion",
+ "s3:ListMultipartUploadParts",
+ "s3:PutObject",
+ "s3:AbortMultipartUpload",
+ "s3:DeleteObject",
+ "s3:DeleteObjectVersion"
+ ],
+ "Resource": [
+ "arn:aws:s3:::/*"
+ ],
+ "Effect": "Allow"
+ }
+ ]
+ }
+ ```
+
+* A public ALB:
+
+ - Create a target group with the instance registered (HTTP on port `8080` and path `/`)
+ - Configure HTTP (port 80) to redirect to HTTPS (port 443)
+ - HTTPS (port 443) using the latest security policy and an ACM certificate like `send.mydomain.com`
+
+* A Route53 public record, alias from `send.mydomain.com` to the ALB
+
+## Software requirements
+
+* Git
+* NodeJS `15.x` LTS
+* Local Redis server
+
+### Prerequisite packages
+
+```bash
+sudo apt update
+sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
+```
+
+### Add repositories
+
+* NodeJS `15.x` LTS (checkout [package.json](../package.json)):
+
+```bash
+curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | sudo apt-key add -
+echo 'deb [arch=amd64] https://deb.nodesource.com/node_15.x focal main' | sudo tee /etc/apt/sources.list.d/nodejs.list
+```
+
+* Git (latest)
+
+```bash
+sudo add-apt-repository ppa:git-core/ppa
+```
+
+* Redis (latest)
+
+```bash
+sudo add-apt-repository ppa:redislabs/redis
+```
+
+### Install required packages
+
+```bash
+sudo apt update
+sudo apt install git nodejs redis-server telnet
+```
+
+### Redis server
+
+#### Password (optional)
+
+Generate a strong password:
+
+```bash
+makepasswd --chars=100
+```
+
+Edit Redis configuration file `/etc/redis/redis.conf`:
+
+```bash
+requirepass
+```
+
+_Note: documentation on securing Redis https://redis.io/topics/security_
+
+#### Systemd
+
+Enable and (re)start the Redis server service:
+
+```bash
+sudo systemctl enable redis-server
+sudo systemctl restart redis-server
+sudo systemctl status redis-server
+```
+
+## Website directory
+
+Setup a directory for the data
+
+```
+sudo mkdir -pv /var/www/send
+sudo chown www-data:www-data /var/www/send
+sudo 750 /var/www/send
+```
+
+### NodeJS
+
+Update npm:
+
+```bash
+sudo npm install -g npm
+```
+
+Checkout current NodeJS and npm versions:
+
+```bash
+node --version
+npm --version
+```
+
+Clone repository, install JavaScript packages and compiles the assets:
+
+```bash
+sudo su -l www-data -s /bin/bash
+cd /var/www/send
+git clone https://gitlab.com/timvisee/send.git .
+npm install
+npm run build
+exit
+```
+
+Create the file `/var/www/send/.env` used by Systemd with your environment variables
+(checkout [config.js](../server/config.js) for more configuration environment variables):
+
+```
+BASE_URL='https://send.mydomain.com'
+NODE_ENV='production'
+PORT='8080'
+REDIS_PASSWORD=''
+S3_BUCKET=''
+```
+
+Lower files and folders permissions to user and group `www-data`:
+
+```
+sudo find /var/www/send -type d -exec chmod 750 {} \;
+sudo find /var/www/send -type f -exec chmod 640 {} \;
+sudo find -L /var/www/send/node_modules/.bin/ -exec chmod 750 {} \;
+```
+
+### Systemd
+
+Create the file `/etc/systemd/system/send.service` with `root` user and `644` mode:
+
+```
+[Unit]
+Description=Send
+After=network.target
+Requires=redis-server.service
+Documentation=https://gitlab.com/timvisee/send
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/npm run prod
+EnvironmentFile=/var/www/send/.env
+WorkingDirectory=/var/www/send
+User=www-data
+Group=www-data
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
+```
+
+_Note: could be better tuner to secure the service by restricting system permissions,
+check with `systemd-analyze security send`_
+
+Enable and start the Send service, check logs:
+
+```
+sudo systemctl daemon-reload
+sudo systemctl enable send
+sudo systemctl start send
+sudo systemctl status send
+journalctl -fu send
+```
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index 548da409..8291a567 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -1,2 +1,2 @@
-# flod as main contact for string changes
-public/locales/en-US/*.ftl @flodolo
+# timvisee as main contact for string changes
+public/locales/en-US/*.ftl @timvisee
diff --git a/docs/acceptance-mobile.md b/docs/acceptance-mobile.md
new file mode 100644
index 00000000..08d0d9c6
--- /dev/null
+++ b/docs/acceptance-mobile.md
@@ -0,0 +1,78 @@
+# Send V2 UX Mobile Acceptance and Spec Annotations
+
+`Date Created: 8/20/2018`
+
+## Acceptance Criteria
+
+Adapted from [this spreadsheet](https://airtable.com/shrkcBPOLkvNFOrpp)
+
+- [ ] It should look and feel of an Android App
+- [ ] It should look and feel like the Send Web Client
+
+### Main Screen
+- [ ] It should clearly Indicate the name of the product
+- [ ] If user has no existing Sends, it should make clear the primary benefits of the service (private, e2e encrypted, self-destructing file sharing)
+- [ ] It should allow users to access the file picker to create Send links
+- [ ] If the user has existing Sends, it should display a card-based list view of each [see Cards section below]
+
+### Non-Authenticated Users
+- [ ] It should make clear the benefits of a Firefox Account
+- [ ] It should allow users to log into or create a Firefox account
+- [ ] It should allow users to select and send multiple files in one URL
+- [ ] It should limit the sendable file size to 1GB
+- [ ] It should allow users to set an expiration time of 5 minutes, 1 hour, or 24 hours
+- [ ] It should allow users to set a download count of 1 downloads
+
+### Authenticated Users
+- [ ] It should indicate that the user is signed in via Firefox Account
+- [ ] It should allow the user to sign out
+- [ ] It should allow users to select and send multiple files in one URL
+- [ ] It should limit users to sending 2.5GB per Send
+- [ ] It should allow users to extend Send times up to 1 Week
+- [ ] It should allow users to extend Send download counts up to 100 times
+
+### Cards
+- [ ] It should display the name of the sent file/files
+- [ ] It should display the time remaining before expiration
+- [ ] It should display the number of downloads remaining before expiration
+- [ ] It should have a button that lets the user copy the send link to their clipboard
+- [ ] It should show a preview icon (not a thumbnail) that has some relationship to the file types or content being sent* (see 5.1 in spec)
+- [ ] It should have an overflow (meatball) menu that when triggered, gives the user share or delete buttons
+- [ ] While encrypting / pushing to server, it should display a progress meter and a cancel button
+- [ ] For authenticated users, it should be expandable to display all files in a send (5.1.1)
+- [ ] If user cancels Send, or Upload fails, it should display a warning in the card
+- [ ] It should display expired Sends below current sends with their UI greyed out and an expiration warning for 24 hours after expiration
+- [ ] It should remove expired cards from display after 24 hours
+- [ ] It should let users permanently delete records expired sends
+- [ ] It should display a visual indicator when a Send is password protected
+- [ ] It should allow the user to share via a native Android share sheet
+- [ ] It should allow me to create Send links through intents from other apps
+
+### General/other
+- [ ] It should allow users to set passwords to protect their Sends
+- [ ] It should warn users when they are trying to upload files larger than their share limit
+
+### Stretch
+- [ ] It should allow users to use the photo gallery to create Send links
+- [ ] It should allow users to use their camera to create Send links
+- [ ] It should allow users to opt into notification when a share link expires
+- [ ] It should allow users to opt into notifications when their link is downloaded
+
+## Annotations on Mobile Spec
+This document tracks differences between the UX spec for Send and the intended MVP.
+
+[Spec Link](https://mozilla.invisionapp.com/share/GNN6KKOQ5XS)
+
+* 1.1: Spec describes toolbar which may not be possible given the application framework we're using. In particular, issues with the spec include the color, logo and different font weights may be an issue.
+* 1.2: Spec's treatment of FxA UI may be difficult to match. We should use the default OAuth implementation and re-evaluate UX once we see an implementation demo. Also, the landing page UI should display a log-in CTA directly and not require users click into the hamburger menu.
+* 2.1: MVP will only include file picker. Signed in users will be able to select multiple files. File selection flow will be Android-native. Probably don't have the ability to add notifications as in the last screen on this page.
+* 2.1: @fzzzy will provide screenshots of this flow for UX evaluation and comment.
+* 3.1.4: The spec shows deleting the last item in an unshared set returning the user to the picker menu. Instead, it should return to the app home page.
+* 3.1.5: Same as 3.1.5 notes. Both cases should show the warning dialog.
+* 4.1: We may not be able to do a thumbnail here. Instead we should specify a set of icons to be displayed.
+* 6.3: We're not going to allow cards to be edited. This page is deprecated.
+* 6.4: Swiping cards to delete is stretched.
+* 6.5: We're not 100% sure what happens on network connectivity errors, we should test this and adapt UX as necessary.
+* 7.1: The last screen on this page depicts a network error notification on the selection screen. Instead the user should hit the send button, be taken back to the cards and display the card as in 5.1.2
+* 7.3: May not be necessary...we can ask for permissions on install.
+* 8.1: Notifications do not block launch
diff --git a/docs/acceptance-web.md b/docs/acceptance-web.md
new file mode 100644
index 00000000..859ef2fd
--- /dev/null
+++ b/docs/acceptance-web.md
@@ -0,0 +1,73 @@
+# Send V2 UX Web Acceptance Criteria
+
+## General
+
+- [ ] It should match the spec provided.
+- [ ] It should have a feedback button
+- [ ] It should provide links to relevant legal documentation
+
+### Non-Authenticated Users
+
+- [ ] It should make clear the benefits of a Firefox Account
+- [ ] It should allow users to log into or create a Firefox account
+- [ ] It should allow users to select and send multiple files in one URL
+- [ ] It should limit the sendable file size to 1GB
+- [ ] It should allow users to set an expiration time of 5 minutes, 1 hour, or 24 hours
+- [ ] It should allow users to set an download count of 1 downloads
+
+### Authenticated Users
+
+- [ ] It should indicate that the user is signed in via Firefox Account
+- [ ] It should allow the user to sign out
+- [ ] It should allow users to select and send multiple files in one URL
+- [ ] It should limit users to sending 2.5GB per Send
+- [ ] It should allow users to extend Send times up to 1 Week
+- [ ] It should allow users to extend Send download counts up to 100 times
+
+### Main Screen
+
+- [ ] It should clearly indicate the name of the product
+- [ ] If user has no existing Sends, it should make clear the primary benefits of the service (private, e2e encrypted, self-destructing file sharing)
+- [ ] It should allow users to access the file picker to create Send links
+- [ ] It should allow users to drag and drop files
+- [ ] It should provide affordances to sign in to Send
+- [ ] If the user has existing Sends, it should display a card-based list view of each
+
+### Upload UI
+
+- [ ] It should allow users to continue to add files to their upload up to a set limit
+- [ ] It should allow users to set a password
+- [ ] It should let users delete items from their upload bundle
+
+### Uploading UI
+
+- [ ] It should display an affordance to demonstrate the status of an upload
+
+### Share UI
+
+- [ ] It should provide a copiable URL to the bundle
+
+### Download UI
+
+- [ ] It should prompt the user for a password if one is required
+- [ ] It should provide feedback for incorrect passwords
+- [ ] It should provide a description of Send to make clear what this service is
+- [ ] It should let the user see the files they are downloading
+- [ ] It should let the user download their files
+
+### Download Complete UI
+
+- [ ] It should indicate that a download is complete
+- [ ] It should provide a description of the Send service
+- [ ] It should provide a link back to the upload UI
+
+### Expiry UI
+
+- [ ] It should provide a generic message indicating a share has expired
+- [ ] It should allow the user to navigate back to the upload page
+
+### In Memory DL Page
+
+- [ ] It should show in case a user tries to download a large file on a suboptimal client
+- [ ] It should suggest the user use Firefox
+- [ ] It should let the user copy the download url
\ No newline at end of file
diff --git a/docs/build.md b/docs/build.md
index 78ebd66f..f440f410 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -2,11 +2,11 @@ Send has two build configurations, development and production. Both can be run v
# Development
-`npm start` launches a `webpack-dev-server` on port 8080 that compiles the assets and watches files for changes. It also serves the backend API and frontend unit tests via the `server/dev.js` entrypoint. The frontend tests can be run in the browser by navigating to http://localhost:8080/test and will rerun automatically as the watched files are saved with changes.
+`npm start` launches a `webpack-dev-server` on port 8080 that compiles the assets and watches files for changes. It also serves the backend API and frontend unit tests via the `server/bin/dev.js` entrypoint. The frontend tests can be run in the browser by navigating to http://localhost:8080/test and will rerun automatically as the watched files are saved with changes.
# Production
-`npm run build` compiles the assets and writes the files to the `dist/` directory. `npm run prod` launches an Express server on port 1443 that serves the backend API and frontend static assets from `dist/` via the `server/prod.js` entrypoint.
+`npm run build` compiles the assets and writes the files to the `dist/` directory. `npm run prod` launches an Express server on port 1443 that serves the backend API and frontend static assets from `dist/` via the `server/bin/prod.js` entrypoint.
# Notable differences
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 00000000..e4f6f60c
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,96 @@
+## Requirements
+
+This document describes how to do a full deployment of Send on your own Linux server. You will need:
+
+* A working (and ideally somewhat recent) installation of NodeJS and npm
+* Git
+* Apache webserver
+* Optionally telnet, to be able to quickly check your installation
+
+For example in Debian/Ubuntu systems:
+
+```bash
+sudo apt install git apache2 nodejs npm telnet
+```
+
+## Building
+
+* We assume an already configured virtual-host on your webserver with an existing empty htdocs folder
+* First, remove that htdocs folder - we will replace it with Send's version now
+* git clone https://github.com/timvisee/send.git htdocs
+* Make now sure you are NOT root but rather the user your webserver is serving files under (e.g. "su www-data" or whoever the owner of your htdocs folder is)
+* npm install
+* npm run build
+
+## Running
+
+To have a permanently running version of Send as a background process:
+
+* Create a file `run.sh` with:
+
+```bash
+#!/bin/bash
+nohup su www-data -c "npm run prod" 2>/dev/null &
+```
+
+* Execute the script:
+
+```bash
+chmod +x run.sh
+./run.sh
+```
+
+Now the Send backend should be running on port 1443. You can check with:
+
+```bash
+telnet localhost 1443
+```
+
+## Reverse Proxy
+
+Of course, we don't want to expose the service on port 1443. Instead we want our normal webserver to forward all requests to Send ("Reverse proxy").
+
+# Apache webserver
+
+* Enable Apache required modules:
+
+```bash
+sudo a2enmod headers
+sudo a2enmod proxy
+sudo a2enmod proxy_http
+sudo a2enmod proxy_wstunnel
+sudo a2enmod rewrite
+```
+
+* Edit your Apache virtual host configuration file, insert this:
+
+```
+# Enable rewrite engine
+RewriteEngine on
+
+# Make sure the original domain name is forwarded to Send
+# Otherwise the generated URLs will be wrong
+ProxyPreserveHost on
+
+# Make sure the generated URL is https://
+RequestHeader set X-Forwarded-Proto https
+
+# If it's a normal file (e.g. PNG, CSS) just return it
+RewriteCond %{REQUEST_FILENAME} -f
+RewriteRule .* - [L]
+
+# If it's a websocket connection, redirect it to a Send WS connection
+RewriteCond %{HTTP:Upgrade} =websocket [NC]
+RewriteRule /(.*) ws://127.0.0.1:1443/$1 [P,L]
+
+# Otherwise redirect it to a normal HTTP connection
+RewriteRule ^/(.*)$ http://127.0.0.1:1443/$1 [P,QSA]
+ProxyPassReverse "/" "http://127.0.0.1:1443"
+```
+
+* Test configuration and restart Apache:
+
+```bash
+sudo apache2ctl configtest
+sudo systemctl restart apache2
+```
diff --git a/docs/docker.md b/docs/docker.md
index 9003412b..265485a7 100644
--- a/docs/docker.md
+++ b/docs/docker.md
@@ -1,35 +1,160 @@
-## Setup
+## Docker Quickstart
-Before building the Docker image, you must build the production assets:
+Use `registry.gitlab.com/timvisee/send:latest` from [`timvisee/send`'s Gitlab image registry](https://gitlab.com/timvisee/send/container_registry) for the latest Docker image.
-```sh
-npm run build
+```bash
+docker pull registry.gitlab.com/timvisee/send:latest
+
+# example quickstart (point REDIS_HOST to an already-running redis server)
+docker run -v $PWD/uploads:/uploads -p 1443:1443 \
+ -e 'DETECT_BASE_URL=true' \
+ -e 'REDIS_HOST=localhost' \
+ -e 'FILE_DIR=/uploads' \
+ registry.gitlab.com/timvisee/send:latest
```
-Then you can run either `docker build` or `docker-compose up`.
+Or clone this repo and run `docker build -t send:latest .` to build an image locally.
+*Note: for Docker Compose, see: https://github.com/timvisee/send-docker-compose*
-## Environment variables:
+## Environment Variables
-| Name | Description
+All the available config options and their defaults can be found here: https://github.com/timvisee/send/blob/master/server/config.js
+
+Config options should be set as unquoted environment variables. Boolean options should be `true`/`false`, time/duration should be integers (seconds), and filesize values should be integers (bytes).
+
+Config options expecting array values (e.g. `EXPIRE_TIMES_SECONDS`, `DOWNLOAD_COUNTS`) should be in unquoted CSV format. UI dropdowns will default to the first value in the CSV, e.g. `DOWNLOAD_COUNTS=5,1,10,100` will show four dropdown options, with `5` selected by the default.
+
+#### Server Configuration
+
+| Name | Description |
|------------------|-------------|
-| `PORT` | Port the server will listen on (defaults to 1443).
-| `S3_BUCKET` | The S3 bucket name.
-| `REDIS_HOST` | Host name of the Redis server.
-| `GOOGLE_ANALYTICS_ID` | Google Analytics ID
-| `SENTRY_CLIENT` | Sentry Client ID
-| `SENTRY_DSN` | Sentry DSN
-| `MAX_FILE_SIZE` | in bytes (defaults to 2147483648)
-| `NODE_ENV` | "production"
+| `BASE_URL` | The HTTPS URL where traffic will be served (e.g. `https://send.firefox.com`)
+| `DETECT_BASE_URL` | Autodetect the base URL using browser if `BASE_URL` is unset (defaults to `false`)
+| `PORT` | Port the server will listen on (defaults to `1443`)
+| `NODE_ENV` | Run in `development` mode (unsafe) or `production` mode (the default)
+| `SEND_FOOTER_DMCA_URL` | A URL to a contact page for DMCA requests (empty / not shown by default)
+| `SENTRY_CLIENT`, `SENTRY_DSN` | Sentry Client ID and DSN for error tracking (optional, disabled by default)
-## Example:
+*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
-```sh
-$ docker run --net=host -e 'NODE_ENV=production' \
+#### Upload and Download Limits
+
+Configure the limits for uploads and downloads. Long expiration times are risky on public servers as people may use you as free hosting for copyrighted content or malware (which is why Mozilla shut down their `send` service). It's advised to only expose your service on a LAN/intranet, password protect it with a proxy/gateway, or make sure to set `SEND_FOOTER_DMCA_URL` above so you can respond to takedown requests.
+
+| Name | Description |
+|------------------|-------------|
+| `MAX_FILE_SIZE` | Maximum upload file size in bytes (defaults to `2147483648` aka 2GB)
+| `MAX_FILES_PER_ARCHIVE` | Maximum number of files per archive (defaults to `64`)
+| `MAX_EXPIRE_SECONDS` | Maximum upload expiry time in seconds (defaults to `604800` aka 7 days)
+| `MAX_DOWNLOADS` | Maximum number of downloads (defaults to `100`)
+| `DOWNLOAD_COUNTS` | Download limit options to show in UI dropdown, e.g. `10,1,2,5,10,15,25,50,100,1000`
+| `EXPIRE_TIMES_SECONDS` | Expire time options to show in UI dropdown, e.g. `3600,86400,604800,2592000,31536000`
+| `DEFAULT_DOWNLOADS` | Default download limit in UI (defaults to `1`)
+| `DEFAULT_EXPIRE_SECONDS` | Default expire time in UI (defaults to `86400`)
+
+*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
+
+#### Storage Backend Options
+
+Pick how you want to store uploaded files and set these config options accordingly:
+
+- Local filesystem (the default): set `FILE_DIR` to the local path used inside the container for storage (or leave the default)
+- S3-compatible object store: set `S3_BUCKET`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (and `S3_ENDPOINT` if using something other than AWS)
+- Google Cloud Storage: set `GCS_BUCKET` to the name of a GCS bucket (auth should be set up using [Application Default Credentials](https://cloud.google.com/docs/authentication/production#auth-cloud-implicit-nodejs))
+
+Redis is used as the metadata database for the backend and is required no matter which storage method you use.
+
+| Name | Description |
+|------------------|-------------|
+| `REDIS_HOST`, `REDIS_PORT`, `REDIS_USER`, `REDIS_PASSWORD`, `REDIS_DB` | Host name, port, and pass of the Redis server (defaults to `localhost`, `6379`, and no password)
+| `FILE_DIR` | Directory for storage inside the Docker container (defaults to `/uploads`)
+| `S3_BUCKET` | The S3 bucket name to use (only set if using S3 for storage)
+| `S3_ENDPOINT` | An optional custom endpoint to use for S3 (defaults to AWS)
+| `S3_USE_PATH_STYLE_ENDPOINT`| Whether to force [path style URLs](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#s3ForcePathStyle-property) for S3 objects (defaults to `false`)
+| `AWS_ACCESS_KEY_ID` | S3 access key ID (only set if using S3 for storage)
+| `AWS_SECRET_ACCESS_KEY` | S3 secret access key ID (only set if using S3 for storage)
+| `GCS_BUCKET` | Google Cloud Storage bucket (only set if using GCP for storage)
+
+*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
+
+## Branding
+
+To change the look the colors aswell as some graphics can be changed via environment variables.
+See the table below for the variables and their default values.
+
+| Name | Default | Description |
+|---|---|---|
+| UI_COLOR_PRIMARY | #0a84ff | The primary color |
+| UI_COLOR_ACCENT | #003eaa | The accent color (eg. for hover-effects) |
+| UI_CUSTOM_ASSETS_ANDROID_CHROME_192PX | | A custom icon for Android (192x192px) |
+| UI_CUSTOM_ASSETS_ANDROID_CHROME_512PX | | A custom icon for Android (512x512px) |
+| UI_CUSTOM_ASSETS_APPLE_TOUCH_ICON | | A custom icon for Apple |
+| UI_CUSTOM_ASSETS_FAVICON_16PX | | A custom favicon (16x16px) |
+| UI_CUSTOM_ASSETS_FAVICON_32PX | | A custom favicon (32x32px) |
+| UI_CUSTOM_ASSETS_ICON | | A custom icon (Logo on the top-left of the UI) |
+| UI_CUSTOM_ASSETS_SAFARI_PINNED_TAB | | A custom icon for Safari |
+| UI_CUSTOM_ASSETS_FACEBOOK | | A custom header image for Facebook |
+| UI_CUSTOM_ASSETS_TWITTER | | A custom header image for Twitter |
+| UI_CUSTOM_ASSETS_WORDMARK | | A custom wordmark (Text next to the logo) |
+| UI_CUSTOM_CSS | | Allows you to define a custom CSS file for custom styling |
+| CUSTOM_FOOTER_TEXT | | Allows you to define a custom footer |
+| CUSTOM_FOOTER_URL | | Allows you to define a custom URL in your footer |
+
+Side note: If you define a custom URL and a custom footer, only the footer text will display, but will be hyperlinked to the URL.
+
+## Examples
+
+**Run using an Amazon Elasticache for the Redis DB, Amazon S3 for the storage backend, and Sentry for error reporting.**
+
+```bash
+$ docker run -p 1443:1443 \
-e 'S3_BUCKET=testpilot-p2p-dev' \
-e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
- -e 'GOOGLE_ANALYTICS_ID=UA-35433268-78' \
-e 'SENTRY_CLIENT=https://51e23d7263e348a7a3b90a5357c61cb2@sentry.prod.mozaws.net/168' \
-e 'SENTRY_DSN=https://51e23d7263e348a7a3b90a5357c61cb2:65e23d7263e348a7a3b90a5357c61c44@sentry.prod.mozaws.net/168' \
- mozilla/send:latest
+ -e 'BASE_URL=https://send.example.com' \
+ registry.gitlab.com/timvisee/send:latest
```
+
+*Note: make sure to replace the example values above with your real values before running.*
+
+
+**Run totally self-hosted using the current filesystem directry (`$PWD`) to store the Redis data and file uploads, with a `5GB` upload limit, 1 month expiry, and contact URL set.**
+
+```bash
+# create a network for the send backend and redis containers to talk to each other
+$ docker network create timviseesend
+
+# start the redis container
+$ docker run --net=timviseesend -v $PWD/redis:/data redis-server --appendonly yes
+
+# start the send backend container
+$ docker run --net=timviseesend -v $PWD/uploads:/uploads -p 1443:1443 \
+ -e 'BASE_URL=http://localhost:1443' \
+ -e 'MAX_FILE_SIZE=5368709120' \
+ -e 'MAX_EXPIRE_SECONDS=2592000' \
+ -e 'SEND_FOOTER_DMCA_URL=https://example.com/dmca-contact-info' \
+ registry.gitlab.com/timvisee/send:latest
+```
+Then open http://localhost:1443 to view the UI. (change the `localhost` to your IP or hostname above to serve the UI to others)
+
+To run with HTTPS, you will need to set up a reverse proxy with SSL termination in front of the backend. See Docker Compose below for an example setup.
+
+
+**Run with custom branding.**
+
+```bash
+$ docker run -p 1443:1443 \
+ -v $PWD/custom_assets:/app/dist/custom_assets \
+ -e 'UI_COLOR_PRIMARY=#f00' \
+ -e 'UI_COLOR_ACCENT=#a00' \
+ -e 'UI_CUSTOM_ASSETS_ICON=custom_assets/logo.svg' \
+ registry.gitlab.com/timvisee/send:latest
+```
+
+## Docker Compose
+
+For a Docker compose configuration example, see:
+
+https://github.com/timvisee/send-docker-compose
diff --git a/docs/encryption.md b/docs/encryption.md
index d9481eeb..b41d37de 100644
--- a/docs/encryption.md
+++ b/docs/encryption.md
@@ -1,14 +1,14 @@
# File Encryption
-Send use 128-bit AES-GCM encryption via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) to encrypt files in the browser before uploading them to the server. The code is in [app/keychain.js](../app/keychain.js).
+Send uses 128-bit AES-GCM encryption via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) to encrypt files in the browser before uploading them to the server. The code is in [app/keychain.js](../app/keychain.js).
## Steps
### Uploading
1. A new secret key is generated with `crypto.getRandomValues`
-2. The secret key is used to derive 3 more keys via HKDF SHA-256
- - an encryption key for the file (AES-GCM)
+2. The secret key is used to derive more keys via HKDF SHA-256
+ - a series of encryption keys for the file, via [ECE](https://tools.ietf.org/html/rfc8188) (AES-GCM)
- an encryption key for the file metadata (AES-GCM)
- a signing key for request authentication (HMAC SHA-256)
3. The file and metadata are encrypted with their corresponding keys
@@ -21,7 +21,7 @@ Send use 128-bit AES-GCM encryption via the [Web Crypto API](https://developer.m
1. The browser loads the share url page, which includes an authentication nonce
2. The browser imports the secret key from the url fragment
3. The same 3 keys as above are derived
-4. The browser signs the nonce with it's signing key and requests the metadata
+4. The browser signs the nonce with its signing key and requests the metadata
5. The encrypted metadata is decrypted and presented on the page
6. The browser makes another authenticated request to download the encrypted file
7. The browser downloads and decrypts the file
diff --git a/docs/faq.md b/docs/faq.md
index 7809e48f..657042de 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -1,12 +1,12 @@
-## How big of a file can I transfer with Firefox Send?
+## How big of a file can I transfer with Send?
-There is a 2GB file size limit built in to Send, however, in practice you may
-be unable to send files that large. Send encrypts and decrypts the files in
-the browser which is great for security but will tax your system resources. In
-particular you can expect to see your memory usage go up by at least the size
-of the file when the transfer is processing. You can see [the results of some
-testing](https://github.com/mozilla/send/issues/170#issuecomment-314107793).
-For the most reliable operation on common computers, it’s probably best to stay
+There is a 2GB file size limit built in to Send, but this may be changed by the
+hoster. Send encrypts and decrypts the files in the browser which is great for
+security but will tax your system resources. In particular you can expect to
+see your memory usage go up by at least the size of the file when the transfer
+is processing. You can see [the results of some
+testing](https://github.com/mozilla/send/issues/170#issuecomment-314107793). For
+the most reliable operation on common computers, it’s probably best to stay
under a few hundred megabytes.
## Why is my browser not supported?
@@ -17,26 +17,25 @@ Many browsers support this standard and should work fine, but some have not
implemented it yet (mobile browsers lag behind on this, in
particular).
-## Why does Firefox Send require JavaScript?
+## Why does Send require JavaScript?
-Firefox Send uses JavaScript to:
+Send uses JavaScript to:
- Encrypt and decrypt files locally on the client instead of the server.
- Render the user interface.
-- Manage translations on the website into [various different languages](https://github.com/mozilla/send#localization).
+- Manage translations on the website into [various different languages](https://github.com/timvisee/send#localization).
- Collect data to help us improve Send in accordance with our [Terms & Privacy](https://send.firefox.com/legal).
-Since Send is an open source project, you can see all of the cool ways we use JavaScript by [examining our code](https://github.com/mozilla/send/).
+Since Send is an open source project, you can see all of the cool ways we use JavaScript by [examining our code](https://github.com/timvisee/send/).
## How long are files available for?
Files are available to be downloaded for 24 hours, after which they are removed
-from the server. They are also removed immediately after a download completes.
+from the server. They are also removed immediately once the download limit is reached.
## Can a file be downloaded more than once?
-Not currently, but we're considering multiple download support in a future
-release.
+Yes, once a file is submitted to Send you can select the download limit.
*Disclaimer: Send is an experiment and under active development. The answers
diff --git a/docs/localization.md b/docs/localization.md
index ad9072e1..1cd92d41 100644
--- a/docs/localization.md
+++ b/docs/localization.md
@@ -1,6 +1,6 @@
# Localization
-Send is localized in over 50 languages. We use the [fluent](http://projectfluent.org/) library and store our translations in [FTL](http://projectfluent.org/fluent/guide/) files in `public/locales/`. `en-US` is our base language, and other languages are managed by [pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/).
+Send is localized in over 50 languages. We use the [fluent](http://projectfluent.org/) library and store our translations in [FTL](http://projectfluent.org/fluent/guide/) files in `public/locales/`. `en-US` is our base language.
## Process
@@ -14,7 +14,7 @@ The development environment includes all locales in `public/locales` via the `L1
## Code
-In `app/` we use the `state.translate()` function to translate strings to the best matching language base on the user's `Accept-Language` header. It's a wrapper around fluent's [MessageContext.format](http://projectfluent.org/fluent.js/fluent/MessageContext.html). It works the same for both server and client side rendering.
+In `app/` we use the `state.translate()` function to translate strings to the best matching language base on the user's `Accept-Language` header. It's a wrapper around fluent's [FluentBundle.format](http://projectfluent.org/fluent.js/fluent/FluentBundle.html). It works the same for both server and client side rendering.
### Examples
diff --git a/docs/metrics.md b/docs/metrics.md
deleted file mode 100644
index abff9189..00000000
--- a/docs/metrics.md
+++ /dev/null
@@ -1,156 +0,0 @@
-# Send Metrics
-The metrics collection and analysis plan for Send, a forthcoming Test Pilot experiment.
-
-## Analysis
-Data collected by Send will be used to answer the following high-level questions:
-
-- Do users send files?
- - How often? How many?
- - What is the retention?
- - What is the distribution of senders?
-- How do recipients interact with promotional UI elements?
- - Are file recipients converted to file senders?
- - Are non-Firefox users converted to Firefox users?
-- Where does it go wrong?
- - How often are there errors in uploading or downloading files?
- - What types of errors to users commonly see?
- - At what point do errors affect retention?
-
-## Collection
-Data will be collected with Google Analytics and follow [Test Pilot standards](https://github.com/mozilla/testpilot/blob/master/docs/experiments/ga.md) for reporting.
-
-### Custom Metrics
-- `cm1` - the size of the file, in bytes.
-- `cm2` - the amount of time it took to complete the file transfer, in milliseconds. Only include if the file completed transferring (ref: `cd2`).
-- `cm3` - the rate of the file transfer, in bytes per second. This is computed by dividing `cm1` by `cm2`, not by monitoring transfer speeds. Only include if the file completed transferring (ref: `cd2`).
-- `cm4` - the amount of time until the file will expire, in milliseconds.
-- `cm5` - the number of files the user has ever uploaded.
-- `cm6` - the number of unexpired files the user has uploaded.
-- `cm7` - the number of files the user has ever downloaded.
-- `cm8` - the number of downloads permitted by the uploader.
-
-### Custom Dimensions
-- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
-- `cd2` - the reason that the file transfer stopped. One of `completed`, `errored`, `cancelled`.
-- `cd3` - the destination of a link click. One of `experiment-page`, `download-firefox`, `twitter`, `github`, `cookies`, `terms`, `privacy`, `about`, `legal`, `mozilla`.
-- `cd4` - the location from which the user copied the URL to an upload file. One of `success-screen`, `upload-list`.
-- `cd5` - the referring location. One of `completed-download`, `errored-download`, `cancelled-download`, `completed-upload`, `errored-upload`, `cancelled-upload`, `testpilot`, `external`.
-- `cd6` - identifying information about an error. Exclude if there is no error involved. **TODO:** enumerate a list of possibilities.
-
-### Events
-
-_NB:_ due to how files are being tracked, there are no events indicating file expiry. This carries some risk: most notably, we can only derive expiration rates by looking at download rates, which is prone to skew if there are problems in data collection.
-
-#### `upload-started`
-Triggered whenever a user begins uploading a file. Includes:
-
-- `ec` - `sender`
-- `ea` - `upload-started`
-- `cm1`
-- `cm5`
-- `cm6`
-- `cm7`
-- `cd1`
-- `cd5`
-
-#### `upload-stopped`
-Triggered whenever a user stops uploading a file. Includes:
-
-- `ec` - `sender`
-- `ea` - `upload-stopped`
-- `cm1`
-- `cm2`
-- `cm3`
-- `cm5`
-- `cm6`
-- `cm7`
-- `cd1`
-- `cd2`
-- `cd6`
-
-#### `download-limit-changed`
-Triggered whenever the sender changes the download limit. Includes:
-
-- `ec` - `sender`
-- `ea` - `download-limit-changed`
-- `cm1`
-- `cm5`
-- `cm6`
-- `cm7`
-- `cm8`
-
-#### `password-added`
-Triggered whenever a password is added to a file. Includes:
-
-- `cm1`
-- `cm5`
-- `cm6`
-- `cm7`
-
-#### `download-started`
-Triggered whenever a user begins downloading a file. Includes:
-
-- `ec` - `recipient`
-- `ea` - `download-started`
-- `cm1`
-- `cm4`
-- `cm5`
-- `cm6`
-- `cm7`
-
-#### `download-stopped`
-Triggered whenever a user stops downloading a file.
-
-- `ec` - `recipient`
-- `ea` - `download-stopped`
-- `cm1`
-- `cm2` (if possible and applicable)
-- `cm3` (if possible and applicable)
-- `cm5`
-- `cm6`
-- `cm7`
-- `cd2`
-- `cd6`
-
-#### `exited`
-Fired whenever a user follows a link external to Send.
-
-- `ec` - `recipient`, `sender`, or `other`, as applicable.
-- `ea` - `exited`
-- `cd3`
-
-#### `upload-deleted`
-Fired whenever a user deletes a file they’ve uploaded.
-
-- `ec` - `sender`
-- `ea` - `upload-deleted`
-- `cm1`
-- `cm2`
-- `cm3`
-- `cm4`
-- `cm5`
-- `cm6`
-- `cm7`
-- `cd1`
-- `cd4`
-
-#### `copied`
-Fired whenever a user copies the URL of an upload file.
-
-- `ec` - `sender`
-- `ea` - `copied`
-- `cd4`
-
-#### `restarted`
-Fired whenever the user interrupts any part of funnel to return to the start of it (e.g. with a “send another file” or “send your own files” link).
-
-- `ec` - `recipient`, `sender`, or `other`, as applicable.
-- `ea` - `restarted`
-- `cd2`
-
-#### `unsupported`
-Fired whenever a user is presented a message saying that their browser is unsupported due to missing crypto APIs.
-
-- `ec` - `recipient` or `sender`, as applicable.
-- `ea` - `unsupported`
-- `cd6`
diff --git a/docs/notes/streams.md b/docs/notes/streams.md
new file mode 100644
index 00000000..ec0c0b89
--- /dev/null
+++ b/docs/notes/streams.md
@@ -0,0 +1,34 @@
+# Web Streams
+
+- API
+ - https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
+- Reference Implementation
+ - https://github.com/whatwg/streams/tree/master/reference-implementation
+- Examples
+ - https://github.com/mdn/dom-examples/tree/master/streams
+- Polyfill
+ - https://github.com/MattiasBuelens/web-streams-polyfill
+
+# Encrypted Content Encoding
+
+- Spec
+ - https://trac.tools.ietf.org/html/rfc8188
+- node.js implementation
+ - https://github.com/web-push-libs/encrypted-content-encoding/tree/master/nodejs
+
+# Other APIs
+
+- Blobs
+ - https://developer.mozilla.org/en-US/docs/Web/API/Blob
+- ArrayBuffers, etc
+ - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
+ - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
+- FileReader
+ - https://developer.mozilla.org/en-US/docs/Web/API/FileReader
+
+# Other
+
+- node.js Buffer browser library
+ - https://github.com/feross/buffer
+- StreamSaver
+ - https://github.com/jimmywarting/StreamSaver.js
diff --git a/ios/generate-bundle.js b/ios/generate-bundle.js
new file mode 100644
index 00000000..70f60c3d
--- /dev/null
+++ b/ios/generate-bundle.js
@@ -0,0 +1,21 @@
+const child_process = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+child_process.execSync('npm run build');
+
+const prefix = path.join('..', 'dist');
+const json_string = fs.readFileSync(path.join(prefix, 'manifest.json'));
+const manifest = JSON.parse(json_string);
+
+const ios_filename = manifest['ios.js'];
+fs.writeFileSync(
+ 'send-ios/assets/ios.js',
+ fs.readFileSync(`${prefix}${ios_filename}`)
+);
+
+const vendor_filename = manifest['vendor.js'];
+fs.writeFileSync(
+ 'send-ios/assets/vendor.js',
+ fs.readFileSync(`${prefix}${vendor_filename}`)
+);
diff --git a/ios/ios.js b/ios/ios.js
new file mode 100644
index 00000000..23b7c4a3
--- /dev/null
+++ b/ios/ios.js
@@ -0,0 +1,158 @@
+/* global window, document, fetch */
+
+const MAXFILESIZE = 1024 * 1024 * 1024 * 2;
+
+const EventEmitter = require('events');
+const emitter = new EventEmitter();
+
+function dom(tagName, attributes, children = []) {
+ const node = document.createElement(tagName);
+ for (const name in attributes) {
+ if (name.indexOf('on') === 0) {
+ node[name] = attributes[name];
+ } else if (name === 'htmlFor') {
+ node.htmlFor = attributes.htmlFor;
+ } else if (name === 'className') {
+ node.className = attributes.className;
+ } else {
+ node.setAttribute(name, attributes[name]);
+ }
+ }
+ if (!(children instanceof Array)) {
+ children = [children];
+ }
+ for (let child of children) {
+ if (typeof child === 'string') {
+ child = document.createTextNode(child);
+ }
+ node.appendChild(child);
+ }
+ return node;
+}
+
+function uploadComplete(file) {
+ document.body.innerHTML = '';
+ const input = dom('input', { id: 'url', value: file.url });
+ const copy = dom(
+ 'button',
+ {
+ id: 'copy-button',
+ className: 'button',
+ onclick: () => {
+ window.webkit.messageHandlers['copy'].postMessage(input.value);
+ copy.textContent = 'Copied!';
+ setTimeout(function() {
+ copy.textContent = 'Copy to clipboard';
+ }, 2000);
+ }
+ },
+ 'Copy to clipboard'
+ );
+ const node = dom(
+ 'div',
+ { id: 'striped' },
+ dom('div', { id: 'white' }, [
+ input,
+ copy,
+ dom(
+ 'button',
+ { id: 'send-another', className: 'button', onclick: render },
+ 'Send another file'
+ )
+ ])
+ );
+ document.body.appendChild(node);
+}
+
+const state = {
+ storage: {
+ files: [],
+ remove: function(fileId) {
+ console.log('REMOVE FILEID', fileId);
+ },
+ writeFile: function(file) {
+ console.log('WRITEFILE', file);
+ },
+ addFile: uploadComplete,
+ totalUploads: 0
+ },
+ transfer: null,
+ uploading: false,
+ settingPassword: false,
+ passwordSetError: null,
+ route: '/'
+};
+
+function upload(event) {
+ console.log('UPLOAD');
+ event.preventDefault();
+ const target = event.target;
+ const file = target.files[0];
+ if (file.size === 0) {
+ return;
+ }
+ if (file.size > MAXFILESIZE) {
+ console.log('file too big (no bigger than ' + MAXFILESIZE + ')');
+ return;
+ }
+
+ emitter.emit('upload', { file: file, type: 'click' });
+}
+
+function render() {
+ document.body.innerHTML = '';
+ const striped = dom(
+ 'div',
+ { id: 'striped' },
+ dom('div', { id: 'white' }, [
+ dom('label', { id: 'label', htmlFor: 'input' }, 'Choose file'),
+ dom('input', {
+ id: 'input',
+ type: 'file',
+ name: 'input',
+ onchange: upload
+ })
+ ])
+ );
+ document.body.appendChild(striped);
+}
+
+emitter.on('render', function() {
+ document.body.innerHTML = '';
+ const percent =
+ (state.transfer.progress[0] / state.transfer.progress[1]) * 100;
+ const node = dom(
+ 'div',
+ { style: 'background-color: white; width: 100%' },
+ dom('span', {
+ style: `display: inline-block; width: ${percent}%; background-color: blue`
+ })
+ );
+ document.body.appendChild(node);
+});
+
+emitter.on('pushState', function(path) {
+ console.log('pushState ' + path + ' ' + JSON.stringify(state));
+});
+
+const controller = require('../app/controller').default;
+try {
+ controller(state, emitter);
+} catch (e) {
+ console.error('error' + e);
+ console.error(e);
+}
+
+function sendBase64EncodedFromSwift(encoded) {
+ fetch(encoded)
+ .then(res => res.blob())
+ .then(blob => {
+ emitter.emit('upload', { file: blob, type: 'share' });
+ });
+}
+
+window.sendBase64EncodedFromSwift = sendBase64EncodedFromSwift;
+
+render();
+
+window.webkit.messageHandlers['loaded'].postMessage('');
diff --git a/ios/send-ios-action-extension/ActionViewController.swift b/ios/send-ios-action-extension/ActionViewController.swift
new file mode 100644
index 00000000..9b202adb
--- /dev/null
+++ b/ios/send-ios-action-extension/ActionViewController.swift
@@ -0,0 +1,77 @@
+//
+// ActionViewController.swift
+// send-ios-action-extension
+//
+// Created by Donovan Preston on 7/26/18.
+//
+
+import UIKit
+import WebKit
+import MobileCoreServices
+
+var typesToLoad = [("com.adobe.pdf", "application/pdf"), ("public.png", "image/png"),
+ ("public.jpeg", "image/jpeg"), ("public.jpeg-2000", "image/jp2"),
+ ("com.compuserve.gif", "image/gif"), ("com.microsoft.bmp", "image/bmp"),
+ ("public.plain-text", "text/plain")]
+
+class ActionViewController: UIViewController, WKScriptMessageHandler {
+
+ @IBOutlet var webView: WKWebView!
+ var typeToSend: String?
+ var dataToSend: Data?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ self.webView.frame = self.view.bounds
+ self.webView?.configuration.userContentController.add(self, name: "loaded")
+ self.webView?.configuration.userContentController.add(self, name: "copy")
+
+ if let url = Bundle.main.url(
+ forResource: "index",
+ withExtension: "html",
+ subdirectory: "assets") {
+ self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
+ }
+ // Get the item[s] we're handling from the extension context.
+
+ for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
+ for provider in item.attachments! as! [NSItemProvider] {
+ for (type, mimeType) in typesToLoad {
+ if provider.hasItemConformingToTypeIdentifier(type) {
+ provider.loadDataRepresentation(forTypeIdentifier: type, completionHandler: { (data, error) in
+ OperationQueue.main.addOperation {
+ self.typeToSend = mimeType
+ self.dataToSend = data
+ }
+ })
+ return
+ }
+ }
+ }
+ }
+ }
+
+ public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+ print("Message received: \(message.name) with body: \(message.body)")
+ if (message.name == "loaded") {
+ let stringToSend = "window.sendBase64EncodedFromSwift('data:\(self.typeToSend ?? "application/octet-stream");base64,\(self.dataToSend?.base64EncodedString() ?? "")')";
+ self.webView.evaluateJavaScript(stringToSend) { (object: Any?, error: Error?) -> Void in
+ print("completed")
+ }
+ } else if (message.name == "copy") {
+ UIPasteboard.general.string = "\(message.body)"
+ }
+ }
+
+ override func didReceiveMemoryWarning() {
+ super.didReceiveMemoryWarning()
+ // Dispose of any resources that can be recreated.
+ }
+
+ @IBAction func done() {
+ // Return any edited content to the host app.
+ // This template doesn't do anything, so we just echo the passed in items.
+ self.extensionContext!.completeRequest(returningItems: self.extensionContext!.inputItems, completionHandler: nil)
+ }
+
+}
diff --git a/ios/send-ios-action-extension/Base.lproj/MainInterface.storyboard b/ios/send-ios-action-extension/Base.lproj/MainInterface.storyboard
new file mode 100644
index 00000000..5939032d
--- /dev/null
+++ b/ios/send-ios-action-extension/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios-action-extension/Info.plist b/ios/send-ios-action-extension/Info.plist
new file mode 100644
index 00000000..10caa73e
--- /dev/null
+++ b/ios/send-ios-action-extension/Info.plist
@@ -0,0 +1,53 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ send-ios-action-extension
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ XPC!
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+ SUBQUERY (
+ extensionItems,
+ $extensionItem,
+ SUBQUERY (
+ $extensionItem.attachments,
+ $attachment,
+ (
+ ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg-2000"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.compuserve.gif"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.microsoft.bmp"
+ )
+ ).@count == 1
+ ).@count == 1
+
+ NSExtensionMainStoryboard
+ MainInterface
+ NSExtensionPointIdentifier
+ com.apple.ui-services
+
+
+
diff --git a/ios/send-ios.xcodeproj/project.pbxproj b/ios/send-ios.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..83f460b2
--- /dev/null
+++ b/ios/send-ios.xcodeproj/project.pbxproj
@@ -0,0 +1,526 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ E34149C621017A3A00930775 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34149C521017A3A00930775 /* AppDelegate.swift */; };
+ E34149C821017A3A00930775 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34149C721017A3A00930775 /* ViewController.swift */; };
+ E34149CB21017A3A00930775 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E34149C921017A3A00930775 /* Main.storyboard */; };
+ E34149CD21017A3D00930775 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E34149CC21017A3D00930775 /* Assets.xcassets */; };
+ E34149D021017A3D00930775 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E34149CE21017A3D00930775 /* LaunchScreen.storyboard */; };
+ E355478521028193009D206E /* help.html in Resources */ = {isa = PBXBuildFile; fileRef = E355478421028193009D206E /* help.html */; };
+ E355478921092E22009D206E /* assets in Resources */ = {isa = PBXBuildFile; fileRef = E355478821092E22009D206E /* assets */; };
+ E355478C210A534F009D206E /* ios.js in Resources */ = {isa = PBXBuildFile; fileRef = E355478B210A534F009D206E /* ios.js */; };
+ E355478E210A5357009D206E /* generate-bundle.js in Resources */ = {isa = PBXBuildFile; fileRef = E355478D210A5357009D206E /* generate-bundle.js */; };
+ E397A0B2210A641C00A978D4 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E397A0B1210A641C00A978D4 /* ActionViewController.swift */; };
+ E397A0B5210A641C00A978D4 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E397A0B3210A641C00A978D4 /* MainInterface.storyboard */; };
+ E397A0B9210A641C00A978D4 /* send-ios-action-extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E397A0AF210A641C00A978D4 /* send-ios-action-extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ E397A0BF210A6B5500A978D4 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = E397A0BE210A6B5500A978D4 /* assets */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ E397A0B7210A641C00A978D4 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = E34149BA21017A3900930775 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = E397A0AE210A641C00A978D4;
+ remoteInfo = "send-ios-action-extension";
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ E397A0BD210A641C00A978D4 /* Embed App Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ E397A0B9210A641C00A978D4 /* send-ios-action-extension.appex in Embed App Extensions */,
+ );
+ name = "Embed App Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ E34149C221017A3900930775 /* send-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "send-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ E34149C521017A3A00930775 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ E34149C721017A3A00930775 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
+ E34149CA21017A3A00930775 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ E34149CC21017A3D00930775 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ E34149CF21017A3D00930775 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ E34149D121017A3D00930775 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ E355478421028193009D206E /* help.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = help.html; sourceTree = ""; };
+ E355478821092E22009D206E /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = ""; };
+ E355478B210A534F009D206E /* ios.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ios.js; sourceTree = SOURCE_ROOT; };
+ E355478D210A5357009D206E /* generate-bundle.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "generate-bundle.js"; sourceTree = SOURCE_ROOT; };
+ E397A0AF210A641C00A978D4 /* send-ios-action-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "send-ios-action-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+ E397A0B1210A641C00A978D4 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; };
+ E397A0B4210A641C00A978D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; };
+ E397A0B6210A641C00A978D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ E397A0BE210A6B5500A978D4 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = "send-ios/assets"; sourceTree = SOURCE_ROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ E34149BF21017A3900930775 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E397A0AC210A641C00A978D4 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ E34149B921017A3900930775 = {
+ isa = PBXGroup;
+ children = (
+ E34149C421017A3900930775 /* send-ios */,
+ E397A0B0210A641C00A978D4 /* send-ios-action-extension */,
+ E34149C321017A3900930775 /* Products */,
+ );
+ sourceTree = "";
+ };
+ E34149C321017A3900930775 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ E34149C221017A3900930775 /* send-ios.app */,
+ E397A0AF210A641C00A978D4 /* send-ios-action-extension.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ E34149C421017A3900930775 /* send-ios */ = {
+ isa = PBXGroup;
+ children = (
+ E355478D210A5357009D206E /* generate-bundle.js */,
+ E355478B210A534F009D206E /* ios.js */,
+ E34149C521017A3A00930775 /* AppDelegate.swift */,
+ E34149C721017A3A00930775 /* ViewController.swift */,
+ E34149C921017A3A00930775 /* Main.storyboard */,
+ E34149CC21017A3D00930775 /* Assets.xcassets */,
+ E34149CE21017A3D00930775 /* LaunchScreen.storyboard */,
+ E34149D121017A3D00930775 /* Info.plist */,
+ E355478421028193009D206E /* help.html */,
+ E355478821092E22009D206E /* assets */,
+ );
+ path = "send-ios";
+ sourceTree = "";
+ };
+ E397A0B0210A641C00A978D4 /* send-ios-action-extension */ = {
+ isa = PBXGroup;
+ children = (
+ E397A0BE210A6B5500A978D4 /* assets */,
+ E397A0B1210A641C00A978D4 /* ActionViewController.swift */,
+ E397A0B3210A641C00A978D4 /* MainInterface.storyboard */,
+ E397A0B6210A641C00A978D4 /* Info.plist */,
+ );
+ path = "send-ios-action-extension";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ E34149C121017A3900930775 /* send-ios */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E34149D421017A3D00930775 /* Build configuration list for PBXNativeTarget "send-ios" */;
+ buildPhases = (
+ E355478A210A4C43009D206E /* ShellScript */,
+ E34149BE21017A3900930775 /* Sources */,
+ E34149BF21017A3900930775 /* Frameworks */,
+ E34149C021017A3900930775 /* Resources */,
+ E397A0BD210A641C00A978D4 /* Embed App Extensions */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ E397A0B8210A641C00A978D4 /* PBXTargetDependency */,
+ );
+ name = "send-ios";
+ productName = "send-ios";
+ productReference = E34149C221017A3900930775 /* send-ios.app */;
+ productType = "com.apple.product-type.application";
+ };
+ E397A0AE210A641C00A978D4 /* send-ios-action-extension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E397A0BC210A641C00A978D4 /* Build configuration list for PBXNativeTarget "send-ios-action-extension" */;
+ buildPhases = (
+ E397A0AB210A641C00A978D4 /* Sources */,
+ E397A0AC210A641C00A978D4 /* Frameworks */,
+ E397A0AD210A641C00A978D4 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "send-ios-action-extension";
+ productName = "send-ios-action-extension";
+ productReference = E397A0AF210A641C00A978D4 /* send-ios-action-extension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ E34149BA21017A3900930775 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0940;
+ LastUpgradeCheck = 0940;
+ ORGANIZATIONNAME = "Donovan Preston";
+ TargetAttributes = {
+ E34149C121017A3900930775 = {
+ CreatedOnToolsVersion = 9.4.1;
+ };
+ E397A0AE210A641C00A978D4 = {
+ CreatedOnToolsVersion = 9.4.1;
+ };
+ };
+ };
+ buildConfigurationList = E34149BD21017A3900930775 /* Build configuration list for PBXProject "send-ios" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = E34149B921017A3900930775;
+ productRefGroup = E34149C321017A3900930775 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ E34149C121017A3900930775 /* send-ios */,
+ E397A0AE210A641C00A978D4 /* send-ios-action-extension */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ E34149C021017A3900930775 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E355478C210A534F009D206E /* ios.js in Resources */,
+ E355478921092E22009D206E /* assets in Resources */,
+ E355478E210A5357009D206E /* generate-bundle.js in Resources */,
+ E34149D021017A3D00930775 /* LaunchScreen.storyboard in Resources */,
+ E355478521028193009D206E /* help.html in Resources */,
+ E34149CD21017A3D00930775 /* Assets.xcassets in Resources */,
+ E34149CB21017A3A00930775 /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E397A0AD210A641C00A978D4 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E397A0B5210A641C00A978D4 /* MainInterface.storyboard in Resources */,
+ E397A0BF210A6B5500A978D4 /* assets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ E355478A210A4C43009D206E /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "node generate-bundle.js\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ E34149BE21017A3900930775 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E34149C821017A3A00930775 /* ViewController.swift in Sources */,
+ E34149C621017A3A00930775 /* AppDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E397A0AB210A641C00A978D4 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E397A0B2210A641C00A978D4 /* ActionViewController.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ E397A0B8210A641C00A978D4 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = E397A0AE210A641C00A978D4 /* send-ios-action-extension */;
+ targetProxy = E397A0B7210A641C00A978D4 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ E34149C921017A3A00930775 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E34149CA21017A3A00930775 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ E34149CE21017A3D00930775 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E34149CF21017A3D00930775 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+ E397A0B3210A641C00A978D4 /* MainInterface.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E397A0B4210A641C00A978D4 /* Base */,
+ );
+ name = MainInterface.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ E34149D221017A3D00930775 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.4;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ E34149D321017A3D00930775 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.4;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ E34149D521017A3D00930775 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "send-ios/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.mozilla.send-ios";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 4.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ E34149D621017A3D00930775 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "send-ios/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.mozilla.send-ios";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 4.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ E397A0BA210A641C00A978D4 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "send-ios-action-extension/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.mozilla.send-ios.send-ios-action-extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 4.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ E397A0BB210A641C00A978D4 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "send-ios-action-extension/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.mozilla.send-ios.send-ios-action-extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 4.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ E34149BD21017A3900930775 /* Build configuration list for PBXProject "send-ios" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E34149D221017A3D00930775 /* Debug */,
+ E34149D321017A3D00930775 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E34149D421017A3D00930775 /* Build configuration list for PBXNativeTarget "send-ios" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E34149D521017A3D00930775 /* Debug */,
+ E34149D621017A3D00930775 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E397A0BC210A641C00A978D4 /* Build configuration list for PBXNativeTarget "send-ios-action-extension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E397A0BA210A641C00A978D4 /* Debug */,
+ E397A0BB210A641C00A978D4 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = E34149BA21017A3900930775 /* Project object */;
+}
diff --git a/ios/send-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/send-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..883bc6f4
--- /dev/null
+++ b/ios/send-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/send-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/send-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/ios/send-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/donovan.xcuserdatad/UserInterfaceState.xcuserstate b/ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/donovan.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 00000000..ccfe2abd
Binary files /dev/null and b/ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/donovan.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
new file mode 100644
index 00000000..a53fd357
--- /dev/null
+++ b/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 00000000..566d89e1
--- /dev/null
+++ b/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,19 @@
+
+
+
+
+ SchemeUserState
+
+ send-ios-action-extension.xcscheme
+
+ orderHint
+ 1
+
+ send-ios.xcscheme
+
+ orderHint
+ 0
+
+
+
+
diff --git a/ios/send-ios/AppDelegate.swift b/ios/send-ios/AppDelegate.swift
new file mode 100644
index 00000000..bfb62d7a
--- /dev/null
+++ b/ios/send-ios/AppDelegate.swift
@@ -0,0 +1,45 @@
+//
+// AppDelegate.swift
+// send-ios
+//
+// Created by Donovan Preston on 7/19/18.
+//
+
+import UIKit
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+ // Override point for customization after application launch.
+ return true
+ }
+
+ func applicationWillResignActive(_ application: UIApplication) {
+ // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+ // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
+ }
+
+ func applicationDidEnterBackground(_ application: UIApplication) {
+ // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+ // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+ }
+
+ func applicationWillEnterForeground(_ application: UIApplication) {
+ // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
+ }
+
+ func applicationDidBecomeActive(_ application: UIApplication) {
+ // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+ }
+
+ func applicationWillTerminate(_ application: UIApplication) {
+ // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+ }
+
+
+}
+
diff --git a/ios/send-ios/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/send-ios/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..d8db8d65
--- /dev/null
+++ b/ios/send-ios/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,98 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "83.5x83.5",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "size" : "1024x1024",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/ios/send-ios/Assets.xcassets/Contents.json b/ios/send-ios/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..da4a164c
--- /dev/null
+++ b/ios/send-ios/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/ios/send-ios/Base.lproj/LaunchScreen.storyboard b/ios/send-ios/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 00000000..f83f6fd5
--- /dev/null
+++ b/ios/send-ios/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios/Base.lproj/Main.storyboard b/ios/send-ios/Base.lproj/Main.storyboard
new file mode 100644
index 00000000..09ae84fd
--- /dev/null
+++ b/ios/send-ios/Base.lproj/Main.storyboard
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios/Info.plist b/ios/send-ios/Info.plist
new file mode 100644
index 00000000..16be3b68
--- /dev/null
+++ b/ios/send-ios/Info.plist
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/ios/send-ios/ViewController.swift b/ios/send-ios/ViewController.swift
new file mode 100644
index 00000000..289f07c4
--- /dev/null
+++ b/ios/send-ios/ViewController.swift
@@ -0,0 +1,39 @@
+//
+// ViewController.swift
+// send-ios
+//
+// Created by Donovan Preston on 7/19/18.
+//
+
+import UIKit
+import WebKit
+
+class ViewController: UIViewController, WKScriptMessageHandler {
+ @IBOutlet var webView: WKWebView!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ self.webView.frame = self.view.bounds
+ self.webView?.configuration.userContentController.add(self, name: "loaded")
+ self.webView?.configuration.userContentController.add(self, name: "copy")
+ if let url = Bundle.main.url(
+ forResource: "index",
+ withExtension: "html",
+ subdirectory: "assets") {
+ webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
+ }
+ }
+
+ public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+ print("Message received: \(message.name) with body: \(message.body)")
+ UIPasteboard.general.string = "\(message.body)"
+ }
+
+ override func didReceiveMemoryWarning() {
+ super.didReceiveMemoryWarning()
+ // Dispose of any resources that can be recreated.
+ }
+
+
+}
+
diff --git a/ios/send-ios/assets/background_1.jpg b/ios/send-ios/assets/background_1.jpg
new file mode 100644
index 00000000..c92d3fb1
Binary files /dev/null and b/ios/send-ios/assets/background_1.jpg differ
diff --git a/ios/send-ios/assets/index.css b/ios/send-ios/assets/index.css
new file mode 100644
index 00000000..3dd18d2d
--- /dev/null
+++ b/ios/send-ios/assets/index.css
@@ -0,0 +1,84 @@
+body {
+ background: url('background_1.jpg');
+ display: flex;
+ flex-direction: row;
+ flex: auto;
+ justify-content: center;
+ align-items: center;
+ padding: 0 20px;
+ box-sizing: border-box;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+#striped {
+ background-image: repeating-linear-gradient(
+ 45deg,
+ white,
+ white 5px,
+ #ea000e 5px,
+ #ea000e 25px,
+ white 25px,
+ white 30px,
+ #0083ff 30px,
+ #0083ff 50px
+ );
+ height: 350px;
+ width: 480px;
+}
+
+#white {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ height: 100%;
+ background-color: white;
+ margin: 0 10px;
+ padding: 1px 10px 0 10px;
+}
+
+#label {
+ background: #0297f8;
+ border: 1px solid #0297f8;
+ color: white;
+ font-size: 24px;
+ font-weight: 500;
+ height: 60px;
+ width: 200px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+#input {
+ display: none;
+}
+
+#url {
+ flex: 1;
+ width: 100%;
+ height: 32px;
+ font-size: 24px;
+ margin-top: 1em;
+}
+
+.button {
+ flex: 1;
+ display: block;
+ background: #0297f8;
+ border: 1px solid #0297f8;
+ color: white;
+ font-size: 24px;
+ font-weight: 500;
+ width: 95%;
+ height: 32px;
+ margin-top: 1em;
+}
+
+#send-another {
+ margin-bottom: 1em;
+}
diff --git a/ios/send-ios/assets/index.html b/ios/send-ios/assets/index.html
new file mode 100644
index 00000000..4579be02
--- /dev/null
+++ b/ios/send-ios/assets/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Send
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios/help.html b/ios/send-ios/help.html
new file mode 100644
index 00000000..afbd39d5
--- /dev/null
+++ b/ios/send-ios/help.html
@@ -0,0 +1,5 @@
+
+
+ HELLO WORLD
+
+