Merge branch 'master' into gh-pages

Conflicts:
	index.html
gh-pages
David Cormier 11 years ago
commit cf1459b469
  1. 31
      3rdparty/inflate.min.js
  2. 6
      3rdparty/inflate.min.js.map
  3. 2
      README.md
  4. 4
      bower.json
  5. 12
      css/glowingbear.css
  6. 88
      index.html
  7. 41
      js/filters.js
  8. 166
      js/glowingbear.js
  9. 7
      js/handlers.js
  10. 7
      js/inputbar.js
  11. 80
      js/localstorage.js
  12. 96
      js/models.js
  13. 34
      js/notifications.js
  14. 12
      js/plugin-directive.js
  15. 333
      js/plugins.js
  16. 68
      js/settings.js
  17. 2
      manifest.json
  18. 11
      manifest.webapp
  19. 4
      min.js
  20. 2
      min.map
  21. 4
      package.json
  22. 2
      test/karma.conf.js
  23. 65
      test/unit/filters.js
  24. 24
      test/unit/plugins.js

@ -1,16 +1,15 @@
/** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */(function() {'use strict';function m(b){throw b;}var n=void 0,r=this;function s(b,d){var a=b.split("."),c=r;!(a[0]in c)&&c.execScript&&c.execScript("var "+a[0]);for(var f;a.length&&(f=a.shift());)!a.length&&d!==n?c[f]=d:c=c[f]?c[f]:c[f]={}};var u="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array;function v(b){var d=b.length,a=0,c=Number.POSITIVE_INFINITY,f,e,g,h,k,l,q,p,t;for(p=0;p<d;++p)b[p]>a&&(a=b[p]),b[p]<c&&(c=b[p]);f=1<<a;e=new (u?Uint32Array:Array)(f);g=1;h=0;for(k=2;g<=a;){for(p=0;p<d;++p)if(b[p]===g){l=0;q=h;for(t=0;t<g;++t)l=l<<1|q&1,q>>=1;for(t=l;t<f;t+=k)e[t]=g<<16|p;++h}++g;h<<=1;k<<=1}return[e,a,c]};function w(b,d){this.g=[];this.h=32768;this.d=this.f=this.a=this.l=0;this.input=u?new Uint8Array(b):b;this.m=!1;this.i=x;this.r=!1;if(d||!(d={}))d.index&&(this.a=d.index),d.bufferSize&&(this.h=d.bufferSize),d.bufferType&&(this.i=d.bufferType),d.resize&&(this.r=d.resize);switch(this.i){case y:this.b=32768;this.c=new (u?Uint8Array:Array)(32768+this.h+258);break;case x:this.b=0;this.c=new (u?Uint8Array:Array)(this.h);this.e=this.z;this.n=this.v;this.j=this.w;break;default:m(Error("invalid inflate mode"))}} /** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */(function() {'use strict';var m=this;function q(c,d){var a=c.split("."),b=m;!(a[0]in b)&&b.execScript&&b.execScript("var "+a[0]);for(var e;a.length&&(e=a.shift());)!a.length&&void 0!==d?b[e]=d:b=b[e]?b[e]:b[e]={}};var s="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function t(c){var d=c.length,a=0,b=Number.POSITIVE_INFINITY,e,f,g,h,k,l,p,n,r,K;for(n=0;n<d;++n)c[n]>a&&(a=c[n]),c[n]<b&&(b=c[n]);e=1<<a;f=new (s?Uint32Array:Array)(e);g=1;h=0;for(k=2;g<=a;){for(n=0;n<d;++n)if(c[n]===g){l=0;p=h;for(r=0;r<g;++r)l=l<<1|p&1,p>>=1;K=g<<16|n;for(r=l;r<e;r+=k)f[r]=K;++h}++g;h<<=1;k<<=1}return[f,a,b]};function u(c,d){this.g=[];this.h=32768;this.d=this.f=this.a=this.l=0;this.input=s?new Uint8Array(c):c;this.m=!1;this.i=v;this.s=!1;if(d||!(d={}))d.index&&(this.a=d.index),d.bufferSize&&(this.h=d.bufferSize),d.bufferType&&(this.i=d.bufferType),d.resize&&(this.s=d.resize);switch(this.i){case w:this.b=32768;this.c=new (s?Uint8Array:Array)(32768+this.h+258);break;case v:this.b=0;this.c=new (s?Uint8Array:Array)(this.h);this.e=this.A;this.n=this.w;this.j=this.z;break;default:throw Error("invalid inflate mode");
var y=0,x=1,z={t:y,s:x}; }}var w=0,v=1,x={u:w,t:v};
w.prototype.k=function(){for(;!this.m;){var b=A(this,3);b&1&&(this.m=!0);b>>>=1;switch(b){case 0:var d=this.input,a=this.a,c=this.c,f=this.b,e=n,g=n,h=n,k=c.length,l=n;this.d=this.f=0;e=d[a++];e===n&&m(Error("invalid uncompressed block header: LEN (first byte)"));g=e;e=d[a++];e===n&&m(Error("invalid uncompressed block header: LEN (second byte)"));g|=e<<8;e=d[a++];e===n&&m(Error("invalid uncompressed block header: NLEN (first byte)"));h=e;e=d[a++];e===n&&m(Error("invalid uncompressed block header: NLEN (second byte)"));h|= u.prototype.k=function(){for(;!this.m;){var c=y(this,3);c&1&&(this.m=!0);c>>>=1;switch(c){case 0:var d=this.input,a=this.a,b=this.c,e=this.b,f=d.length,g=void 0,h=void 0,k=b.length,l=void 0;this.d=this.f=0;if(a+1>=f)throw Error("invalid uncompressed block header: LEN");g=d[a++]|d[a++]<<8;if(a+1>=f)throw Error("invalid uncompressed block header: NLEN");h=d[a++]|d[a++]<<8;if(g===~h)throw Error("invalid uncompressed block header: length verify");if(a+g>d.length)throw Error("input buffer is broken");switch(this.i){case w:for(;e+
e<<8;g===~h&&m(Error("invalid uncompressed block header: length verify"));a+g>d.length&&m(Error("input buffer is broken"));switch(this.i){case y:for(;f+g>c.length;){l=k-f;g-=l;if(u)c.set(d.subarray(a,a+l),f),f+=l,a+=l;else for(;l--;)c[f++]=d[a++];this.b=f;c=this.e();f=this.b}break;case x:for(;f+g>c.length;)c=this.e({p:2});break;default:m(Error("invalid inflate mode"))}if(u)c.set(d.subarray(a,a+g),f),f+=g,a+=g;else for(;g--;)c[f++]=d[a++];this.a=a;this.b=f;this.c=c;break;case 1:this.j(B,C);break;case 2:aa(this); g>b.length;){l=k-e;g-=l;if(s)b.set(d.subarray(a,a+l),e),e+=l,a+=l;else for(;l--;)b[e++]=d[a++];this.b=e;b=this.e();e=this.b}break;case v:for(;e+g>b.length;)b=this.e({p:2});break;default:throw Error("invalid inflate mode");}if(s)b.set(d.subarray(a,a+g),e),e+=g,a+=g;else for(;g--;)b[e++]=d[a++];this.a=a;this.b=e;this.c=b;break;case 1:this.j(z,A);break;case 2:B(this);break;default:throw Error("unknown BTYPE: "+c);}}return this.n()};
break;default:m(Error("unknown BTYPE: "+b))}}return this.n()}; var C=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],D=s?new Uint16Array(C):C,E=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,258,258],F=s?new Uint16Array(E):E,G=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0],H=s?new Uint8Array(G):G,I=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],J=s?new Uint16Array(I):I,L=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,
var D=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],E=u?new Uint16Array(D):D,F=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,258,258],G=u?new Uint16Array(F):F,H=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0],I=u?new Uint8Array(H):H,J=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],K=u?new Uint16Array(J):J,L=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13, 13],M=s?new Uint8Array(L):L,N=new (s?Uint8Array:Array)(288),O,P;O=0;for(P=N.length;O<P;++O)N[O]=143>=O?8:255>=O?9:279>=O?7:8;var z=t(N),Q=new (s?Uint8Array:Array)(30),R,S;R=0;for(S=Q.length;R<S;++R)Q[R]=5;var A=t(Q);function y(c,d){for(var a=c.f,b=c.d,e=c.input,f=c.a,g=e.length,h;b<d;){if(f>=g)throw Error("input buffer is broken");a|=e[f++]<<b;b+=8}h=a&(1<<d)-1;c.f=a>>>d;c.d=b-d;c.a=f;return h}
13],M=u?new Uint8Array(L):L,N=new (u?Uint8Array:Array)(288),O,P;O=0;for(P=N.length;O<P;++O)N[O]=143>=O?8:255>=O?9:279>=O?7:8;var B=v(N),Q=new (u?Uint8Array:Array)(30),R,S;R=0;for(S=Q.length;R<S;++R)Q[R]=5;var C=v(Q);function A(b,d){for(var a=b.f,c=b.d,f=b.input,e=b.a,g;c<d;)g=f[e++],g===n&&m(Error("input buffer is broken")),a|=g<<c,c+=8;g=a&(1<<d)-1;b.f=a>>>d;b.d=c-d;b.a=e;return g} function T(c,d){for(var a=c.f,b=c.d,e=c.input,f=c.a,g=e.length,h=d[0],k=d[1],l,p;b<k&&!(f>=g);)a|=e[f++]<<b,b+=8;l=h[a&(1<<k)-1];p=l>>>16;c.f=a>>p;c.d=b-p;c.a=f;return l&65535}
function T(b,d){for(var a=b.f,c=b.d,f=b.input,e=b.a,g=d[0],h=d[1],k,l,q;c<h;){k=f[e++];if(k===n)break;a|=k<<c;c+=8}l=g[a&(1<<h)-1];q=l>>>16;b.f=a>>q;b.d=c-q;b.a=e;return l&65535} function B(c){function d(a,c,b){var d,e=this.q,f,g;for(g=0;g<a;)switch(d=T(this,c),d){case 16:for(f=3+y(this,2);f--;)b[g++]=e;break;case 17:for(f=3+y(this,3);f--;)b[g++]=0;e=0;break;case 18:for(f=11+y(this,7);f--;)b[g++]=0;e=0;break;default:e=b[g++]=d}this.q=e;return b}var a=y(c,5)+257,b=y(c,5)+1,e=y(c,4)+4,f=new (s?Uint8Array:Array)(D.length),g,h,k,l;for(l=0;l<e;++l)f[D[l]]=y(c,3);if(!s){l=e;for(e=f.length;l<e;++l)f[D[l]]=0}g=t(f);h=new (s?Uint8Array:Array)(a);k=new (s?Uint8Array:Array)(b);c.q=0;
function aa(b){function d(a,b,c){var d,e,f,g;for(g=0;g<a;)switch(d=T(this,b),d){case 16:for(f=3+A(this,2);f--;)c[g++]=e;break;case 17:for(f=3+A(this,3);f--;)c[g++]=0;e=0;break;case 18:for(f=11+A(this,7);f--;)c[g++]=0;e=0;break;default:e=c[g++]=d}return c}var a=A(b,5)+257,c=A(b,5)+1,f=A(b,4)+4,e=new (u?Uint8Array:Array)(E.length),g,h,k,l;for(l=0;l<f;++l)e[E[l]]=A(b,3);g=v(e);h=new (u?Uint8Array:Array)(a);k=new (u?Uint8Array:Array)(c);b.j(v(d.call(b,a,g,h)),v(d.call(b,c,g,k)))} c.j(t(d.call(c,a,g,h)),t(d.call(c,b,g,k)))}u.prototype.j=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length-258,f,g,h,k;256!==(f=T(this,c));)if(256>f)b>=e&&(this.b=b,a=this.e(),b=this.b),a[b++]=f;else{g=f-257;k=F[g];0<H[g]&&(k+=y(this,H[g]));f=T(this,d);h=J[f];0<M[f]&&(h+=y(this,M[f]));b>=e&&(this.b=b,a=this.e(),b=this.b);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b};
w.prototype.j=function(b,d){var a=this.c,c=this.b;this.o=b;for(var f=a.length-258,e,g,h,k;256!==(e=T(this,b));)if(256>e)c>=f&&(this.b=c,a=this.e(),c=this.b),a[c++]=e;else{g=e-257;k=G[g];0<I[g]&&(k+=A(this,I[g]));e=T(this,d);h=K[e];0<M[e]&&(h+=A(this,M[e]));c>=f&&(this.b=c,a=this.e(),c=this.b);for(;k--;)a[c]=a[c++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=c}; u.prototype.z=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length,f,g,h,k;256!==(f=T(this,c));)if(256>f)b>=e&&(a=this.e(),e=a.length),a[b++]=f;else{g=f-257;k=F[g];0<H[g]&&(k+=y(this,H[g]));f=T(this,d);h=J[f];0<M[f]&&(h+=y(this,M[f]));b+k>e&&(a=this.e(),e=a.length);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b};
w.prototype.w=function(b,d){var a=this.c,c=this.b;this.o=b;for(var f=a.length,e,g,h,k;256!==(e=T(this,b));)if(256>e)c>=f&&(a=this.e(),f=a.length),a[c++]=e;else{g=e-257;k=G[g];0<I[g]&&(k+=A(this,I[g]));e=T(this,d);h=K[e];0<M[e]&&(h+=A(this,M[e]));c+k>f&&(a=this.e(),f=a.length);for(;k--;)a[c]=a[c++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=c}; u.prototype.e=function(){var c=new (s?Uint8Array:Array)(this.b-32768),d=this.b-32768,a,b,e=this.c;if(s)c.set(e.subarray(32768,c.length));else{a=0;for(b=c.length;a<b;++a)c[a]=e[a+32768]}this.g.push(c);this.l+=c.length;if(s)e.set(e.subarray(d,d+32768));else for(a=0;32768>a;++a)e[a]=e[d+a];this.b=32768;return e};
w.prototype.e=function(){var b=new (u?Uint8Array:Array)(this.b-32768),d=this.b-32768,a,c,f=this.c;if(u)b.set(f.subarray(32768,b.length));else{a=0;for(c=b.length;a<c;++a)b[a]=f[a+32768]}this.g.push(b);this.l+=b.length;if(u)f.set(f.subarray(d,d+32768));else for(a=0;32768>a;++a)f[a]=f[d+a];this.b=32768;return f}; u.prototype.A=function(c){var d,a=this.input.length/this.a+1|0,b,e,f,g=this.input,h=this.c;c&&("number"===typeof c.p&&(a=c.p),"number"===typeof c.v&&(a+=c.v));2>a?(b=(g.length-this.a)/this.o[2],f=258*(b/2)|0,e=f<h.length?h.length+f:h.length<<1):e=h.length*a;s?(d=new Uint8Array(e),d.set(h)):d=h;return this.c=d};
w.prototype.z=function(b){var d,a=this.input.length/this.a+1|0,c,f,e,g=this.input,h=this.c;b&&("number"===typeof b.p&&(a=b.p),"number"===typeof b.u&&(a+=b.u));2>a?(c=(g.length-this.a)/this.o[2],e=258*(c/2)|0,f=e<h.length?h.length+e:h.length<<1):f=h.length*a;u?(d=new Uint8Array(f),d.set(h)):d=h;return this.c=d}; u.prototype.n=function(){var c=0,d=this.c,a=this.g,b,e=new (s?Uint8Array:Array)(this.l+(this.b-32768)),f,g,h,k;if(0===a.length)return s?this.c.subarray(32768,this.b):this.c.slice(32768,this.b);f=0;for(g=a.length;f<g;++f){b=a[f];h=0;for(k=b.length;h<k;++h)e[c++]=b[h]}f=32768;for(g=this.b;f<g;++f)e[c++]=d[f];this.g=[];return this.buffer=e};
w.prototype.n=function(){var b=0,d=this.c,a=this.g,c,f=new (u?Uint8Array:Array)(this.l+(this.b-32768)),e,g,h,k;if(0===a.length)return u?this.c.subarray(32768,this.b):this.c.slice(32768,this.b);e=0;for(g=a.length;e<g;++e){c=a[e];h=0;for(k=c.length;h<k;++h)f[b++]=c[h]}e=32768;for(g=this.b;e<g;++e)f[b++]=d[e];this.g=[];return this.buffer=f}; u.prototype.w=function(){var c,d=this.b;s?this.s?(c=new Uint8Array(d),c.set(this.c.subarray(0,d))):c=this.c.subarray(0,d):(this.c.length>d&&(this.c.length=d),c=this.c);return this.buffer=c};function U(c,d){var a,b;this.input=c;this.a=0;if(d||!(d={}))d.index&&(this.a=d.index),d.verify&&(this.B=d.verify);a=c[this.a++];b=c[this.a++];switch(a&15){case V:this.method=V;break;default:throw Error("unsupported compression method");}if(0!==((a<<8)+b)%31)throw Error("invalid fcheck flag:"+((a<<8)+b)%31);if(b&32)throw Error("fdict flag is not supported");this.r=new u(c,{index:this.a,bufferSize:d.bufferSize,bufferType:d.bufferType,resize:d.resize})}
w.prototype.v=function(){var b,d=this.b;u?this.r?(b=new Uint8Array(d),b.set(this.c.subarray(0,d))):b=this.c.subarray(0,d):(this.c.length>d&&(this.c.length=d),b=this.c);return this.buffer=b};function U(b,d){var a,c;this.input=b;this.a=0;if(d||!(d={}))d.index&&(this.a=d.index),d.verify&&(this.A=d.verify);a=b[this.a++];c=b[this.a++];switch(a&15){case V:this.method=V;break;default:m(Error("unsupported compression method"))}0!==((a<<8)+c)%31&&m(Error("invalid fcheck flag:"+((a<<8)+c)%31));c&32&&m(Error("fdict flag is not supported"));this.q=new w(b,{index:this.a,bufferSize:d.bufferSize,bufferType:d.bufferType,resize:d.resize})} U.prototype.k=function(){var c=this.input,d,a;d=this.r.k();this.a=this.r.a;if(this.B){a=(c[this.a++]<<24|c[this.a++]<<16|c[this.a++]<<8|c[this.a++])>>>0;var b=d;if("string"===typeof b){var e=b.split(""),f,g;f=0;for(g=e.length;f<g;f++)e[f]=(e[f].charCodeAt(0)&255)>>>0;b=e}for(var h=1,k=0,l=b.length,p,n=0;0<l;){p=1024<l?1024:l;l-=p;do h+=b[n++],k+=h;while(--p);h%=65521;k%=65521}if(a!==(k<<16|h)>>>0)throw Error("invalid adler-32 checksum");}return d};var V=8;q("Zlib.Inflate",U);q("Zlib.Inflate.prototype.decompress",U.prototype.k);var W={ADAPTIVE:x.t,BLOCK:x.u},X,Y,Z,$;if(Object.keys)X=Object.keys(W);else for(Y in X=[],Z=0,W)X[Z++]=Y;Z=0;for($=X.length;Z<$;++Z)Y=X[Z],q("Zlib.Inflate.BufferType."+Y,W[Y]);}).call(this); //@ sourceMappingURL=inflate.min.js.map
U.prototype.k=function(){var b=this.input,d,a;d=this.q.k();this.a=this.q.a;if(this.A){a=(b[this.a++]<<24|b[this.a++]<<16|b[this.a++]<<8|b[this.a++])>>>0;var c=d;if("string"===typeof c){var f=c.split(""),e,g;e=0;for(g=f.length;e<g;e++)f[e]=(f[e].charCodeAt(0)&255)>>>0;c=f}for(var h=1,k=0,l=c.length,q,p=0;0<l;){q=1024<l?1024:l;l-=q;do h+=c[p++],k+=h;while(--q);h%=65521;k%=65521}a!==(k<<16|h)>>>0&&m(Error("invalid adler-32 checksum"))}return d};var V=8;s("Zlib.Inflate",U);s("Zlib.Inflate.prototype.decompress",U.prototype.k);var W={ADAPTIVE:z.s,BLOCK:z.t},X,Y,Z,$;if(Object.keys)X=Object.keys(W);else for(Y in X=[],Z=0,W)X[Z++]=Y;Z=0;for($=X.length;Z<$;++Z)Y=X[Z],s("Zlib.Inflate.BufferType."+Y,W[Y]);}).call(this); //@ sourceMappingURL=inflate.min.js.map

File diff suppressed because one or more lines are too long

@ -83,4 +83,4 @@ If you wish to submit code, we try to make the contribution process as simple as
We'd also like to ask you to join our IRC channel, #glowing-bear on freenode, so we can discuss your ideas and changes. We'd also like to ask you to join our IRC channel, #glowing-bear on freenode, so we can discuss your ideas and changes.
If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](http://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app. If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), [twemoji](https://github.com/twitter/twemoji), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](http://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app.

@ -8,8 +8,10 @@
"dependencies": { "dependencies": {
"angular": "1.3.x", "angular": "1.3.x",
"angular-route": "1.3.x", "angular-route": "1.3.x",
"angular-sanitize": "1.3.x",
"angular-touch": "1.3.x",
"angular-loader": "1.3.x", "angular-loader": "1.3.x",
"angular-mocks": "~1.3.x", "angular-mocks": "1.3.x",
"html5-boilerplate": "~4.3.0" "html5-boilerplate": "~4.3.0"
} }
} }

@ -309,6 +309,10 @@ div.embed img.embed {
max-width: 100%; max-width: 100%;
} }
video.embed {
max-width: 100%;
}
div.colourbox { div.colourbox {
display: inline-block; display: inline-block;
border-radius: 3px; border-radius: 3px;
@ -522,6 +526,14 @@ li.buffer.indent.private a {
user-select: none; user-select: none;
} }
/* Scales emoji to font size */
img.emoji {
height: 1em;
width: 1em;
margin: 0 .05em 0 .1em;
vertical-align: -0.1em;
}
/* */ /* */
/* Mobile layout */ /* Mobile layout */
/* */ /* */

@ -7,23 +7,25 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="theme-color" content="#2779d3">
<title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title> <title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen"> <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link rel="shortcut icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png"> <link rel="shortcut icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png"> <link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" > <link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" >
<link href="css/glowingbear.css" rel="stylesheet" media="screen"> <link href="css/glowingbear.css" rel="stylesheet" media="screen">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-route.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-sanitize.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-touch.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-touch.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="//twemoji.maxcdn.com/twemoji.min.js"></script>
<script type="text/javascript" src="3rdparty/inflate.min.js"></script> <script type="text/javascript" src="3rdparty/inflate.min.js"></script>
<script type="text/javascript" src="min.js"></script> <script type="text/javascript" src="min.js"></script>
<script type="text/javascript" src="3rdparty/favico-0.3.5.min.js"></script> <script type="text/javascript" src="3rdparty/favico-0.3.5.min.js"></script>
</head> </head>
<body ng-controller="WeechatCtrl" ng-keydown="handleKeyPress($event)" ng-keyup="handleKeyRelease($event)" ng-keypress="handleKeyPress($event)" ng-class="{'no-overflow': connected}" lang="en-US"> <body ng-controller="WeechatCtrl" ng-keydown="handleKeyPress($event)" ng-keyup="handleKeyRelease($event)" ng-keypress="handleKeyPress($event)" ng-class="{'no-overflow': connected}" lang="en-US">
<link ng-href="css/themes/{{theme}}.css" rel="stylesheet" media="screen" /> <link ng-href="css/themes/{{settings.theme}}.css" rel="stylesheet" media="screen" />
<div ng-hide="connected" class="container"> <div ng-hide="connected" class="container">
<h2> <h2>
<img alt="logo" src="assets/img/glowing-bear.svg"> <img alt="logo" src="assets/img/glowing-bear.svg">
@ -56,10 +58,10 @@
<div class="input-group"> <div class="input-group">
<div class="row no-gutter"> <div class="row no-gutter">
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control favorite-font" id="host" ng-model="host" placeholder="Address" autocapitalize="off"> <input type="text" class="form-control favorite-font" id="host" ng-model="settings.host" placeholder="Address" autocapitalize="off">
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
<input type="text" class="form-control favorite-font" id="port" ng-model="port" placeholder="Port"> <input type="text" class="form-control favorite-font" id="port" ng-model="settings.port" placeholder="Port">
</div> </div>
</div> </div>
</div> </div>
@ -71,19 +73,19 @@
<div class="checkbox"> <div class="checkbox">
<label class="control-label" for="savepassword"> <label class="control-label" for="savepassword">
<input type="checkbox" id="savepassword" ng-model="savepassword"> <input type="checkbox" id="savepassword" ng-model="settings.savepassword">
Save password in your browser Save password in your browser
</label> </label>
</div> </div>
<div class="checkbox" ng-show="savepassword"> <div class="checkbox" ng-show="settings.savepassword">
<label class="control-label" for="autoconnect"> <label class="control-label" for="autoconnect">
<input type="checkbox" id="autoconnect" ng-model="autoconnect"> <input type="checkbox" id="autoconnect" ng-model="settings.autoconnect">
Automatically connect Automatically connect
</label> </label>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label class="control-label" for="ssl"> <label class="control-label" for="ssl">
<input type="checkbox" id="ssl" ng-model="ssl"> <input type="checkbox" id="ssl" ng-model="settings.ssl">
Encryption. Read instructions for help Encryption. Read instructions for help
</label> </label>
</div> </div>
@ -107,7 +109,7 @@
<div>To start using glowing bear, please enable the relay plugin in your WeeChat client: <div>To start using glowing bear, please enable the relay plugin in your WeeChat client:
<pre> <pre>
/set relay.network.password yourpassword /set relay.network.password yourpassword
/relay add weechat {{ port || 9001 }} /relay add weechat {{ settings.port || 9001 }}
</pre> </pre>
<span class="label label-danger">WeeChat version 0.4.2 or higher is required.</span><br> <span class="label label-danger">WeeChat version 0.4.2 or higher is required.</span><br>
The communication goes directly between your browser and your WeeChat relay in plain text. Check the instructions below for help on setting up encrypted communication. The communication goes directly between your browser and your WeeChat relay in plain text. Check the instructions below for help on setting up encrypted communication.
@ -142,18 +144,18 @@
<div id="collapseThree" class="panel-collapse collapse in"> <div id="collapseThree" class="panel-collapse collapse in">
<div class="panel-body"> <div class="panel-body">
<p>If you check the encryption box, the communication between browser and WeeChat will be encrypted with TLS.</p> <p>If you check the encryption box, the communication between browser and WeeChat will be encrypted with TLS.</p>
<p><strong>Note</strong>: If you are using a self-signed certificate, you have to visit <a href="https://{{ host }}:{{ port }}/">https://{{ host || 'weechathost' }}:{{ port || 'relayport' }}/</a> in your browser first to add a security exception. You can close that tab once you confirmed the certificate, no content will appear. The necessity of this process is a bug in <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=594502">Firefox</a> and other browsers.</p> <p><strong>Note</strong>: If you are using a self-signed certificate, you have to visit <a href="https://{{ settings.host }}:{{ settings.port }}/">https://{{ settings.host || 'weechathost' }}:{{ settings.port || 'relayport' }}/</a> in your browser first to add a security exception. You can close that tab once you confirmed the certificate, no content will appear. The necessity of this process is a bug in <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=594502">Firefox</a> and other browsers.</p>
<p><strong>Setup</strong>: If you want to use an encrypted session you first have to set up the relay to use TLS. You basically have two options: a self-signed certificate is easier to set up, but requires manual security exceptions. Using a certificate that is trusted by your browser requires more setup, but offers greater convenience later on and does not require security exceptions. You can find a guide to set up WeeChat with a free trusted certificate from StartSSL <a href="https://4z2.de/2014/07/06/weechat-trusted-relay">here</a>. Should you wish to use a self-signed certificate instead, execute the following commands in a shell on the same host and as the user running WeeChat:</p> <p><strong>Setup</strong>: If you want to use an encrypted session you first have to set up the relay to use TLS. You basically have two options: a self-signed certificate is easier to set up, but requires manual security exceptions. Using a certificate that is trusted by your browser requires more setup, but offers greater convenience later on and does not require security exceptions. You can find a guide to set up WeeChat with a free trusted certificate from StartSSL <a href="https://4z2.de/2014/07/06/weechat-trusted-relay">here</a>. Should you wish to use a self-signed certificate instead, execute the following commands in a shell on the same host and as the user running WeeChat:</p>
<pre> <pre>
$ mkdir -p ~/.weechat/ssl $ mkdir -p ~/.weechat/ssl
$ cd ~/.weechat/ssl $ cd ~/.weechat/ssl
$ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out relay.pem -subj "/CN={{host || 'your weechat host'}}/" $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out relay.pem -subj "/CN={{settings.host || 'your weechat host'}}/"
</pre> </pre>
<p>If WeeChat is already running, you can reload the certificate and private key and set up an encrypted relay on port {{ port || 9001 }} with these WeeChat commands:</p> <p>If WeeChat is already running, you can reload the certificate and private key and set up an encrypted relay on port {{ settings.port || 9001 }} with these WeeChat commands:</p>
<pre> <pre>
/set relay.network.password yourpassword /set relay.network.password yourpassword
/relay sslcertkey /relay sslcertkey
/relay add ssl.weechat {{ port || 9001 }} /relay add ssl.weechat {{ settings.port || 9001 }}
</pre> </pre>
</div> </div>
</div> </div>
@ -173,7 +175,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<p>If you have a recent version of Firefox you can install glowing bear as an app. Click the button to install.</p> <p>If you have a recent version of Firefox you can install glowing bear as an app. Click the button to install.</p>
<button class="btn btn-lg btn-primary" ng-click="install()">Install Firefox app <i class="glyphicon glyphicon-chevron-right"></i></button> <button class="btn btn-lg btn-primary" ng-click="install()">Install Firefox app <i class="glyphicon glyphicon-chevron-right"></i></button>
<h3>Chrome</h3> <h3>Chrome</h3>
<p>To install glowing bear as an app in Chrome, select <kbd>Menu - Add to home screen</kbd> (Android) or <kbd>Menu - Tools - Create Application Shortcuts</kbd> (desktop version).</p> <p>To install glowing bear as an app in Chrome, select <kbd>Menu - Add to home screen</kbd> (Android) or <kbd>Menu - More tools - Create application shortcuts</kbd> (desktop version).</p>
</div> </div>
</div> </div>
</div> </div>
@ -198,11 +200,13 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div id="topbar"> <div id="topbar">
<div class="brand"> <div class="brand">
<a href="#" ng-click="toggleSidebar()"> <a href="#" ng-click="toggleSidebar()">
<img alt="brand" src="assets/img/favicon.png" title="Connected to {{ host }}:{{ port}}"> <img alt="brand" src="assets/img/favicon.png" title="Connected to {{ settings.host }}:{{ settings.port}}">
</a> </a>
<button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button> <button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button>
</div> </div>
<div class="title" ng-bind-html="activeBuffer().title | linky:'_blank' | DOMfilter:'irclinky'" title="{{activeBuffer().title}}"></div> <div class="title" title="{{activeBuffer().rtitle}}">
<span ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></span>
</div>
<div class="actions pull-right vertical-line-left"> <div class="actions pull-right vertical-line-left">
<div class="pull-left"> <div class="pull-left">
<a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu"> <a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu">
@ -222,7 +226,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</form> </form>
</li> </li>
<li class="buffer" ng-class="{'active': buffer.active, 'indent': buffer.indent, 'channel': buffer.type === 'channel', 'channel_hash': buffer.prefix === '#', 'channel_plus': buffer.prefix === '+', 'channel_ampersand': buffer.prefix === '&', 'private': buffer.type === 'private'}" ng-repeat="(key, buffer) in (filteredBuffers = (getBuffers() | toArray:'withidx' | filter:{fullName:search} | filter:hasUnread | orderBy:predicate | getBufferQuickKeys:this))"> <li class="buffer" ng-class="{'active': buffer.active, 'indent': buffer.indent, 'channel': buffer.type === 'channel', 'channel_hash': buffer.prefix === '#', 'channel_plus': buffer.prefix === '+', 'channel_ampersand': buffer.prefix === '&', 'private': buffer.type === 'private'}" ng-repeat="(key, buffer) in (filteredBuffers = (getBuffers() | toArray:'withidx' | filter:{fullName:search} | filter:hasUnread | orderBy:predicate | getBufferQuickKeys:this))">
<a href="#" ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}"> <a ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}">
<span class="badge pull-right" ng-class="{'danger': buffer.notification}" ng-if="buffer.notification || buffer.unread" ng-bind="buffer.notification || buffer.unread"></span> <span class="badge pull-right" ng-class="{'danger': buffer.notification}" ng-if="buffer.notification || buffer.unread" ng-bind="buffer.notification || buffer.unread"></span>
<span class="buffer-quick-key">{{ buffer.$quickKey }}</span> <span class="buffer-quick-key">{{ buffer.$quickKey }}</span>
<span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span> <span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span>
@ -238,7 +242,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</li> </li>
</ul> </ul>
</div> </div>
<table ng-class="{'notimestamp':!showtimestamp,'notimestampseconds':!showtimestampSeconds}"> <table ng-class="{'notimestamp':!settings.showtimestamp,'notimestampseconds':!settings.showtimestampSeconds}">
<tbody> <tbody>
<tr class="bufferline"> <tr class="bufferline">
<td ng-hide="activeBuffer().allLinesFetched" colspan="3"> <td ng-hide="activeBuffer().allLinesFetched" colspan="3">
@ -257,7 +261,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<td class="prefix"><a ng-click="addMention(bufferline.prefix)"><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text"></span></a></td><!-- <td class="prefix"><a ng-click="addMention(bufferline.prefix)"><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text"></span></a></td><!--
--><td class="message"><!-- --><td class="message"><!--
--><div ng-repeat="metadata in ::bufferline.metadata" plugin data="::metadata"></div><!-- --><div ng-repeat="metadata in ::bufferline.metadata" plugin data="::metadata"></div><!--
--><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'inlinecolour'"></span> --><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'emojify':enableJSEmoji | DOMfilter:'inlinecolour' "></span>
</td> </td>
</tr> </tr>
<tr class="readmarker" ng-if="activeBuffer().lastSeen==$index"> <tr class="readmarker" ng-if="activeBuffer().lastSeen==$index">
@ -289,12 +293,12 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="form-group"> <div class="form-group">
<label for="font" class="col-sm-3 control-label make-thinner">Preferred font</label> <label for="font" class="col-sm-3 control-label make-thinner">Preferred font</label>
<div class="col-sm-4"> <div class="col-sm-4">
<input type="text" ng-model="fontfamily" class="form-control" id="font"> <input type="text" ng-model="settings.fontfamily" class="form-control" id="font">
</div> </div>
<label for="size" class="col-sm-1 control-label">Size</label> <label for="size" class="col-sm-1 control-label">Size</label>
<div class="col-sm-2"> <div class="col-sm-2">
<input type="text" ng-model="fontsize" class="form-control" id="size"> <input type="text" ng-model="settings.fontsize" class="form-control" id="size">
</div> </div>
</div> </div>
</form> </form>
@ -304,7 +308,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="form-group"> <div class="form-group">
<label for="theme" class="col-sm-3 control-label make-thinner">Theme</label> <label for="theme" class="col-sm-3 control-label make-thinner">Theme</label>
<div class="col-sm-7"> <div class="col-sm-7">
<select id="theme" class="form-control" ng-model="theme" ng-options="theme for theme in themes"></select> <select id="theme" class="form-control" ng-model="settings.theme" ng-options="theme for theme in themes"></select>
</div> </div>
</div> </div>
</form> </form>
@ -314,7 +318,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="onlyUnread"> <input type="checkbox" ng-model="settings.onlyUnread">
Only show buffers with unread messages Only show buffers with unread messages
</label> </label>
</div> </div>
@ -324,17 +328,17 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="showtimestamp"> <input type="checkbox" ng-model="settings.showtimestamp">
Show timestamps Show timestamps
</label> </label>
</div> </div>
</form> </form>
<ul ng-show="showtimestamp"> <ul ng-show="settings.showtimestamp">
<li> <li>
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="showtimestampSeconds"> <input type="checkbox" ng-model="settings.showtimestampSeconds">
Show seconds Show seconds
</label> </label>
</div> </div>
@ -346,8 +350,8 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="noembed"> <input type="checkbox" ng-model="settings.noembed">
Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden</span> Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden regardless of this choice</span>
</label> </label>
</div> </div>
</form> </form>
@ -356,7 +360,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="hotlistsync"> <input type="checkbox" ng-model="settings.hotlistsync">
Mark messages as read in WeeChat Mark messages as read in WeeChat
</label> </label>
</div> </div>
@ -366,7 +370,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="nonicklist"> <input type="checkbox" ng-model="settings.nonicklist">
Hide nicklist Hide nicklist
</label> </label>
</div> </div>
@ -376,7 +380,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="orderbyserver"> <input type="checkbox" ng-model="settings.orderbyserver">
Hierarchical buffer view (order by server) Hierarchical buffer view (order by server)
</label> </label>
</div> </div>
@ -386,7 +390,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="readlineBindings"> <input type="checkbox" ng-model="settings.readlineBindings">
Enable common readline keybindings in input bar Enable common readline keybindings in input bar
</label> </label>
</div> </div>
@ -396,7 +400,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="useFavico"> <input type="checkbox" ng-model="settings.useFavico">
Display unread count in favicon Display unread count in favicon
</label> </label>
</div> </div>
@ -406,12 +410,22 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="soundnotification"> <input type="checkbox" ng-model="settings.soundnotification">
Play sound on notification Play sound on notification
</label> </label>
</div> </div>
</form> </form>
</li> </li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="enableJSEmoji">
Enable non-native Emoji support <span class="text-muted settings-help">Displays Emoji characters as images</span>
</label>
</div>
</form>
</li>
</ul> </ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

@ -30,12 +30,19 @@ weechat.filter('irclinky', ['$filter', function($filter) {
return text; return text;
} }
// First, escape entities to prevent escaping issues because it's a bad idea
// to parse/modify HTML with regexes, which we do a couple of lines down...
var entities = {"<": "&lt;", ">": "&gt;", '"': '&quot;', "'": '&#39;', "&": "&amp;", "/": '&#x2F;'};
text = text.replace(/[<>"'&\/]/g, function (char) {
return entities[char];
});
// This regex in no way matches all IRC channel names (they could also begin with &, + or an // This regex in no way matches all IRC channel names (they could also begin with &, + or an
// exclamation mark followed by 5 alphanumeric characters, and are bounded in length by 50). // exclamation mark followed by 5 alphanumeric characters, and are bounded in length by 50).
// However, it matches all *common* IRC channels while trying to minimise false positives. // However, it matches all *common* IRC channels while trying to minimise false positives.
// "#1" is much more likely to be "number 1" than "IRC channel #1". // "#1" is much more likely to be "number 1" than "IRC channel #1".
// Thus, we only match channels beginning with a # and having at least one letter in them. // Thus, we only match channels beginning with a # and having at least one letter in them.
var channelRegex = /(^|[\s,.:;?!"'()+@-])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi; var channelRegex = /(^|[\s,.:;?!"'()+@-\~%])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi;
// This is SUPER nasty, but ng-click does not work inside a filter, as the markup has to be $compiled first, which is not possible in filter afaik. // This is SUPER nasty, but ng-click does not work inside a filter, as the markup has to be $compiled first, which is not possible in filter afaik.
// Therefore, get the scope, fire the method, and $apply. Yuck. I sincerely hope someone finds a better way of doing this. // Therefore, get the scope, fire the method, and $apply. Yuck. I sincerely hope someone finds a better way of doing this.
var substitute = '$1<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'$2\'); $scope.$apply();">$2</a>'; var substitute = '$1<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'$2\'); $scope.$apply();">$2</a>';
@ -50,10 +57,12 @@ weechat.filter('inlinecolour', function() {
} }
// only match 6-digit colour codes, 3-digit ones have too many false positives (issue numbers, etc) // only match 6-digit colour codes, 3-digit ones have too many false positives (issue numbers, etc)
var hexColourRegex = /(^|[^&])\#([0-9a-f]{6})($|[^\w'"])/gmi; var hexColourRegex = /(^|[^&])(\#[0-9a-f]{6};?)(?!\w)/gmi;
var substitute = '$1#$2 <div class="colourbox" style="background-color:#$2"></div> $3'; var rgbColourRegex = /(.?)(rgba?\((?:\s*\d+\s*,){2}\s*\d+\s*(?:,\s*[\d.]+\s*)?\);?)/gmi;
var substitute = '$1$2 <div class="colourbox" style="background-color:$2"></div>';
return text.replace(hexColourRegex, substitute); text = text.replace(hexColourRegex, substitute);
text = text.replace(rgbColourRegex, substitute);
return text;
}; };
}); });
@ -64,6 +73,9 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
return text; return text;
} }
// hacky way to pass an extra argument without using .apply, which
// would require assembling an argument array. PERFORMANCE!!!
var extraArgument = (arguments.length > 2) ? arguments[2] : null;
var filterFunction = $filter(filter); var filterFunction = $filter(filter);
var el = document.createElement('div'); var el = document.createElement('div');
el.innerHTML = text; el.innerHTML = text;
@ -71,7 +83,7 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
// Recursive DOM-walking function applying the filter to the text nodes // Recursive DOM-walking function applying the filter to the text nodes
var process = function(node) { var process = function(node) {
if (node.nodeType === 3) { // text node if (node.nodeType === 3) { // text node
var value = filterFunction(node.nodeValue); var value = filterFunction(node.nodeValue, extraArgument);
if (value !== node.nodeValue) { if (value !== node.nodeValue) {
// we changed something. create a new node to replace the current one // we changed something. create a new node to replace the current one
// we could also only add its children but that would probably incur // we could also only add its children but that would probably incur
@ -87,13 +99,15 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
} else { } else {
parent.appendChild(newNode); parent.appendChild(newNode);
} }
return newNode;
} }
} }
// recurse // recurse
if (node === undefined || node === null) return;
node = node.firstChild; node = node.firstChild;
while (node) { while (node) {
process(node); var nextNode = process(node);
node = node.nextSibling; node = (nextNode ? nextNode : node).nextSibling;
} }
}; };
@ -125,4 +139,15 @@ weechat.filter('getBufferQuickKeys', function () {
}; };
}); });
// Emojifis the string using https://github.com/twitter/twemoji
weechat.filter('emojify', function() {
return function(text, enable_JS_Emoji) {
if (enable_JS_Emoji === true) {
return twemoji.parse(text);
} else {
return(text);
}
};
});
})(); })();

@ -1,16 +1,44 @@
(function() { (function() {
'use strict'; 'use strict';
var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch']); var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch'], ['$compileProvider', function($compileProvider) {
// hacky way to be able to find out if we're in debug mode
weechat.compileProvider = $compileProvider;
}]);
weechat.config(['$compileProvider', function ($compileProvider) { weechat.config(['$compileProvider', function ($compileProvider) {
$compileProvider.debugInfoEnabled(false); // hack to determine whether we're executing the tests
if (typeof(it) === "undefined" && typeof(describe) === "undefined") {
$compileProvider.debugInfoEnabled(false);
}
}]); }]);
weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils) { weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', 'settings',
function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) {
$scope.command = ''; $scope.command = '';
$scope.themes = ['dark', 'light']; $scope.themes = ['dark', 'light'];
settings.setDefaults({
'theme': 'dark',
'host': 'localhost',
'port': 9001,
'ssl': (window.location.protocol === "https:"),
'savepassword': false,
'autoconnect': false,
'nonicklist': utils.isMobileUi(),
'noembed': utils.isMobileUi(),
'onlyUnread': false,
'hotlistsync': true,
'orderbyserver': true,
'useFavico': true,
'showtimestamp': true,
'showtimestampSeconds': false,
'fontsize': '14px',
'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'),
'readlineBindings': false
});
$scope.settings = settings;
// From: http://stackoverflow.com/a/18539624 by StackOverflow user "plantian" // From: http://stackoverflow.com/a/18539624 by StackOverflow user "plantian"
$rootScope.countWatchers = function () { $rootScope.countWatchers = function () {
var q = [$rootScope], watchers = 0, scope; var q = [$rootScope], watchers = 0, scope;
@ -66,22 +94,16 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Enable debug mode if "?debug=1" or "?debug=true" is set // Enable debug mode if "?debug=1" or "?debug=true" is set
(function() { (function() {
var hasReloaded = false;
window.location.search.substring(1).split('&').forEach(function(f) { window.location.search.substring(1).split('&').forEach(function(f) {
var segs = f.split('='); var segs = f.split('=');
if (segs[0] === "debug" && ["true", "1"].indexOf(segs[1]) != -1) { if (segs[0] === "debug" && ["true", "1"].indexOf(segs[1]) != -1) {
$rootScope.debugMode = true; $rootScope.debugMode = true;
} else if (segs[0] === "debugReload" && segs[1] === "1") {
hasReloaded = true;
} }
}); });
// If we haven't reloaded yet, do an angular reload with debug infos // If we haven't reloaded yet, do an angular reload with debug infos
// store whether this has happened yet in a GET parameter // store whether this has happened yet in a GET parameter
if ($rootScope.debugMode && !hasReloaded) { if ($rootScope.debugMode && !weechat.compileProvider.debugInfoEnabled()) {
document.location.search += "&debugReload=1"; angular.reloadWithDebugInfo();
setTimeout(function() {
angular.reloadWithDebugInfo();
}, 0);
} }
})(); })();
@ -193,7 +215,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// we will send a /buffer bufferName command every time // we will send a /buffer bufferName command every time
// the user switches a buffer. This will ensure that notifications // the user switches a buffer. This will ensure that notifications
// are cleared in the buffer the user switches to // are cleared in the buffer the user switches to
if ($scope.hotlistsync && ab.fullName) { if (settings.hotlistsync && ab.fullName) {
connection.sendCoreCommand('/buffer ' + ab.fullName); connection.sendCoreCommand('/buffer ' + ab.fullName);
} }
@ -215,12 +237,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.$on('notificationChanged', function() { $rootScope.$on('notificationChanged', function() {
notifications.updateTitle(); notifications.updateTitle();
if ($scope.useFavico && $rootScope.favico) { if (settings.useFavico && $rootScope.favico) {
notifications.updateFavico(); notifications.updateFavico();
} }
}); });
$rootScope.$on('relayDisconnect', function() { $rootScope.$on('relayDisconnect', function() {
// Reset title
$rootScope.pageTitle = '';
$rootScope.notificationStatus = '';
notifications.cancelAll();
models.reinitialize(); models.reinitialize();
$rootScope.$emit('notificationChanged'); $rootScope.$emit('notificationChanged');
$scope.connectbutton = 'Connect'; $scope.connectbutton = 'Connect';
@ -241,70 +268,31 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.iterCandidate = null; $rootScope.iterCandidate = null;
$store.bind($scope, "host", "localhost"); if (settings.savepassword) {
$store.bind($scope, "port", "9001"); $scope.$watch('password', function() {
$store.bind($scope, "proto", "weechat"); settings.password = $scope.password;
$store.bind($scope, "ssl", (window.location.protocol === "https:")); });
$store.bind($scope, "savepassword", false); settings.addCallback('password', function(password) {
if ($scope.savepassword) { $scope.password = password;
$store.bind($scope, "password", ""); });
$scope.password = settings.password;
} else {
settings.password = '';
} }
$store.bind($scope, "autoconnect", false);
// If we are on mobile change some defaults
// We use 968 px as the cutoff, which should match the value in glowingbear.css
var nonicklist = false;
var noembed = false;
var showtimestamp = true;
$rootScope.wasMobileUi = false; $rootScope.wasMobileUi = false;
if (utils.isMobileUi()) { if (utils.isMobileUi()) {
nonicklist = true;
noembed = true;
$rootScope.wasMobileUi = true; $rootScope.wasMobileUi = true;
} }
if (!settings.fontfamily) {
// Save setting for displaying only buffers with unread messages
$store.bind($scope, "onlyUnread", false);
// Save setting for syncing hotlist
$store.bind($scope, "hotlistsync", true);
// Save setting for displaying nicklist
$store.bind($scope, "nonicklist", nonicklist);
// Save setting for displaying embeds
$store.bind($scope, "noembed", noembed);
// Save setting for channel ordering
$store.bind($scope, "orderbyserver", true);
// Save setting for updating favicon
$store.bind($scope, "useFavico", true);
// Save setting for showtimestamp
$store.bind($scope, "showtimestamp", showtimestamp);
// Save setting for showing seconds on timestamps
$store.bind($scope, "showtimestampSeconds", false);
// Save setting for playing sound on notification
$store.bind($scope, "soundnotification", false);
// Save setting for font family
$store.bind($scope, "fontfamily");
// Save setting for theme
$store.bind($scope, "theme", 'dark');
// Save setting for font size
$store.bind($scope, "fontsize", "14px");
// Save setting for readline keybindings
$store.bind($scope, "readlineBindings", false);
if (!$scope.fontfamily) {
if (utils.isMobileUi()) { if (utils.isMobileUi()) {
$scope.fontfamily = 'sans-serif'; settings.fontfamily = 'sans-serif';
} else { } else {
$scope.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace"; settings.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace";
} }
} }
// Save setting for displaying embeds in rootScope so it can be used from service
$rootScope.auto_display_embedded_content = $scope.noembed === false;
$scope.isSidebarVisible = function() { $scope.isSidebarVisible = function() {
return document.getElementById('content').getAttribute('sidebar-state') === 'visible'; return document.getElementById('content').getAttribute('sidebar-state') === 'visible';
}; };
@ -326,9 +314,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
document.getElementById('content').setAttribute('sidebar-state', 'hidden'); document.getElementById('content').setAttribute('sidebar-state', 'hidden');
} }
}; };
// This also fires on page load settings.addCallback('autoconnect', function(autoconnect) {
$scope.$watch('autoconnect', function() { if (autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) {
if ($scope.autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) {
$scope.connect(); $scope.connect();
} }
}); });
@ -347,35 +334,31 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Open and close panels while on mobile devices through swiping // Open and close panels while on mobile devices through swiping
$scope.openNick = function() { $scope.openNick = function() {
if (utils.isMobileUi()) { if (utils.isMobileUi()) {
if ($scope.nonicklist) { if (settings.nonicklist) {
$scope.nonicklist = false; settings.nonicklist = false;
} }
} }
}; };
$scope.closeNick = function() { $scope.closeNick = function() {
if (utils.isMobileUi()) { if (utils.isMobileUi()) {
if (!$scope.nonicklist) { if (!settings.nonicklist) {
$scope.nonicklist = true; settings.nonicklist = true;
} }
} }
}; };
// Watch model and update show setting when it changes
$scope.$watch('noembed', function() {
$rootScope.auto_display_embedded_content = $scope.noembed === false;
});
// Watch model and update channel sorting when it changes // Watch model and update channel sorting when it changes
$scope.$watch('orderbyserver', function() { settings.addCallback('orderbyserver', function(orderbyserver) {
$rootScope.predicate = $scope.orderbyserver ? 'serverSortKey' : 'number'; $rootScope.predicate = orderbyserver ? 'serverSortKey' : 'number';
}); });
$scope.$watch('useFavico', function() { settings.addCallback('useFavico', function(useFavico) {
// this check is necessary as this is called on page load, too // this check is necessary as this is called on page load, too
if (!$rootScope.connected) { if (!$rootScope.connected) {
return; return;
} }
if ($scope.useFavico) { if (useFavico) {
notifications.updateFavico(); notifications.updateFavico();
} else { } else {
$rootScope.favico.reset(); $rootScope.favico.reset();
@ -383,17 +366,12 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}); });
// Update font family when changed // Update font family when changed
$scope.$watch('fontfamily', function() { settings.addCallback('fontfamily', function(fontfamily) {
utils.changeClassStyle('favorite-font', 'fontFamily', $scope.fontfamily); utils.changeClassStyle('favorite-font', 'fontFamily', fontfamily);
}); });
// Update font size when changed // Update font size when changed
$scope.$watch('fontsize', function() { settings.addCallback('fontsize', function(fontsize) {
utils.changeClassStyle('favorite-font', 'fontSize', $scope.fontsize); utils.changeClassStyle('favorite-font', 'fontSize', fontsize);
});
// Crude scoping hack. The keypress listener does not live in the same scope as
// the checkbox, so we need to transfer this between scopes here.
$scope.$watch('readlineBindings', function() {
$rootScope.readlineBindings = $scope.readlineBindings;
}); });
$scope.setActiveBuffer = function(bufferId, key) { $scope.setActiveBuffer = function(bufferId, key) {
@ -519,7 +497,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.errorMessage = false; $rootScope.errorMessage = false;
$rootScope.bufferBottom = true; $rootScope.bufferBottom = true;
$scope.connectbutton = 'Connecting ...'; $scope.connectbutton = 'Connecting ...';
connection.connect($scope.host, $scope.port, $scope.password, $scope.ssl); connection.connect(settings.host, settings.port, $scope.password, settings.ssl);
}; };
$scope.disconnect = function() { $scope.disconnect = function() {
$scope.connectbutton = 'Connect'; $scope.connectbutton = 'Connect';
@ -587,7 +565,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
if ($scope.search && $scope.search !== "") { if ($scope.search && $scope.search !== "") {
return true; return true;
} }
if ($scope.onlyUnread) { if (settings.onlyUnread) {
// Always show current buffer in list // Always show current buffer in list
if (models.getActiveBuffer() === buffer) { if (models.getActiveBuffer() === buffer) {
return true; return true;
@ -602,7 +580,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}; };
// Watch model and update show setting when it changes // Watch model and update show setting when it changes
$scope.$watch('nonicklist', function() { settings.addCallback('nonicklist', function() {
$scope.showNicklist = $scope.updateShowNicklist(); $scope.showNicklist = $scope.updateShowNicklist();
// restore bottom view // restore bottom view
if ($rootScope.connected && $rootScope.bufferBottom) { if ($rootScope.connected && $rootScope.bufferBottom) {
@ -621,7 +599,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
return false; return false;
} }
// Check if option no nicklist is set // Check if option no nicklist is set
if ($scope.nonicklist) { if (settings.nonicklist) {
return false; return false;
} }
// Check if nicklist is empty // Check if nicklist is empty
@ -655,7 +633,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}; };
// Helper function since the keypress handler is in a different scope // Helper function since the keypress handler is in a different scope
$rootScope.toggleNicklist = function() { $rootScope.toggleNicklist = function() {
$scope.nonicklist = !$scope.nonicklist; settings.nonicklist = !settings.nonicklist;
}; };

@ -64,8 +64,13 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
var buffer = obj.pointers[0]; var buffer = obj.pointers[0];
var old = models.getBuffer(buffer); var old = models.getBuffer(buffer);
old.fullName = obj.full_name; old.fullName = obj.full_name;
old.title = obj.title; old.title = models.parseRichText(obj.title);
old.number = obj.number; old.number = obj.number;
old.rtitle = "";
for (var i = 0; i < old.title.length; ++i) {
old.rtitle += old.title[i].text;
}
}; };
var handleBufferRenamed = function(message) { var handleBufferRenamed = function(message) {

@ -14,13 +14,14 @@ weechat.directive('inputBar', function() {
command: '=command' command: '=command'
}, },
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', function($rootScope, controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', 'settings', function($rootScope,
$scope, $scope,
$element, //XXX do we need this? don't seem to be using it $element, //XXX do we need this? don't seem to be using it
$log, $log,
connection, //XXX we should eliminate this dependency and use signals instead connection, //XXX we should eliminate this dependency and use signals instead
models, models,
IrcUtils) { IrcUtils,
settings) {
/* /*
* Returns the input element * Returns the input element
@ -340,7 +341,7 @@ weechat.directive('inputBar', function() {
} }
// Some readline keybindings // Some readline keybindings
if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { if (settings.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) {
// get current caret position // get current caret position
caretPos = inputNode.selectionStart; caretPos = inputNode.selectionStart;
// Ctrl-a // Ctrl-a

@ -10,6 +10,10 @@ ls.factory("$store", ["$parse", function($parse){
var storage = (typeof window.localStorage === 'undefined') ? undefined : window.localStorage, var storage = (typeof window.localStorage === 'undefined') ? undefined : window.localStorage,
supported = !(typeof storage == 'undefined' || typeof window.JSON == 'undefined'); supported = !(typeof storage == 'undefined' || typeof window.JSON == 'undefined');
if (!supported) {
console.log('Warning: localStorage is not supported');
}
var privateMethods = { var privateMethods = {
/** /**
* Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object) * Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object)
@ -29,7 +33,7 @@ ls.factory("$store", ["$parse", function($parse){
if (val === 'false'){ if (val === 'false'){
val = false; val = false;
} }
if (parseFloat(val) === val && !angular.isObject(val)) { if (parseFloat(val) == val && !angular.isObject(val)) {
val = parseFloat(val); val = parseFloat(val);
} }
} catch(e){ } catch(e){
@ -40,77 +44,73 @@ ls.factory("$store", ["$parse", function($parse){
}; };
var publicMethods = { var publicMethods = {
/** /**
* Set - let's you set a new localStorage key pair set * Set - lets you set a new localStorage key pair set
* @param key - a string that will be used as the accessor for the pair * @param key - a string that will be used as the accessor for the pair
* @param value - the value of the localStorage item * @param value - the value of the localStorage item
* @returns {*} - will return whatever it is you've stored in the local storage * @returns {*} - will return whatever it is you've stored in the local storage
*/ */
set: function(key,value){ set: function(key,value){
if (!supported){ if (!supported){
try { console.log('Local Storage not supported');
$.cookie(key, value);
return value;
} catch(e){
console.log('Local Storage not supported, make sure you have the $.cookie supported.');
}
} }
var saver = JSON.stringify(value); var saver = JSON.stringify(value);
storage.setItem(key, saver); storage.setItem(key, saver);
return privateMethods.parseValue(saver); return privateMethods.parseValue(saver);
}, },
/** /**
* Get - let's you get the value of any pair you've stored * Get - lets you get the value of any pair you've stored
* @param key - the string that you set as accessor for the pair * @param key - the string that you set as accessor for the pair
* @returns {*} - Object,String,Float,Boolean depending on what you stored * @returns {*} - Object,String,Float,Boolean depending on what you stored
*/ */
get: function(key){ get: function(key){
if (!supported){ if (!supported){
try { return null;
return privateMethods.parseValue($.cookie(key));
} catch(e){
return null;
}
} }
var item = storage.getItem(key); var item = storage.getItem(key);
return privateMethods.parseValue(item); return privateMethods.parseValue(item);
}, },
/** /**
* Remove - let's you nuke a value from localStorage * Remove - lets you nuke a value from localStorage
* @param key - the accessor value * @param key - the accessor value
* @returns {boolean} - if everything went as planned * @returns {boolean} - if everything went as planned
*/ */
remove: function(key) { remove: function(key) {
if (!supported){ if (!supported){
try { return false;
$.cookie(key, null);
return true;
} catch(e){
return false;
}
} }
storage.removeItem(key); storage.removeItem(key);
return true; return true;
}, },
/** /**
* Bind - let's you directly bind a localStorage value to a $scope variable * Enumerate all keys
* @param $scope - the current scope you want the variable available in */
* @param key - the name of the variable you are binding enumerateKeys: function() {
* @param def - the default value (OPTIONAL) var keys = [];
* @returns {*} - returns whatever the stored value is for (var i = 0, len = storage.length; i < len; ++i) {
*/ keys.push(storage.key(i));
bind: function ($scope, key, def) {
if (def === undefined) {
def = '';
}
if (publicMethods.get(key) === undefined || publicMethods.get(key) === null) {
publicMethods.set(key, def);
}
$parse(key).assign($scope, publicMethods.get(key));
$scope.$watch(key, function (val) {
publicMethods.set(key, val);
}, true);
return publicMethods.get(key);
} }
return keys;
},
/**
* Bind - lets you directly bind a localStorage value to a $scope variable
* @param $scope - the current scope you want the variable available in
* @param key - the name of the variable you are binding
* @param def - the default value (OPTIONAL)
* @returns {*} - returns whatever the stored value is
*/
bind: function ($scope, key, def) {
if (def === undefined) {
def = '';
}
if (publicMethods.get(key) === undefined || publicMethods.get(key) === null) {
publicMethods.set(key, def);
}
$parse(key).assign($scope, publicMethods.get(key));
$scope.$watch(key, function (val) {
publicMethods.set(key, val);
}, true);
return publicMethods.get(key);
}
}; };
return publicMethods; return publicMethods;
}]); }]);

@ -8,6 +8,48 @@
var models = angular.module('weechatModels', []); var models = angular.module('weechatModels', []);
models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) { models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) {
var parseRichText = function(text) {
var textElements = weeChat.Protocol.rawText2Rich(text),
typeToClassPrefixFg = {
'option': 'cof-',
'weechat': 'cwf-',
'ext': 'cef-'
},
typeToClassPrefixBg = {
'option': 'cob-',
'weechat': 'cwb-',
'ext': 'ceb-'
};
textElements.forEach(function(textEl) {
textEl.classes = [];
// foreground color
var prefix = typeToClassPrefixFg[textEl.fgColor.type];
textEl.classes.push(prefix + textEl.fgColor.name);
// background color
prefix = typeToClassPrefixBg[textEl.bgColor.type];
textEl.classes.push(prefix + textEl.bgColor.name);
// attributes
if (textEl.attrs.name !== null) {
textEl.classes.push('coa-' + textEl.attrs.name);
}
var attr, val;
for (attr in textEl.attrs.override) {
val = textEl.attrs.override[attr];
if (val) {
textEl.classes.push('a-' + attr);
} else {
textEl.classes.push('a-no-' + attr);
}
}
});
return textElements;
};
this.parseRichText = parseRichText;
/* /*
* Buffer class * Buffer class
*/ */
@ -21,7 +63,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var trimmedName = shortName.replace(/^[#&+]/, '') || (shortName ? ' ' : null); var trimmedName = shortName.replace(/^[#&+]/, '') || (shortName ? ' ' : null);
// get channel identifier // get channel identifier
var prefix = ['#', '&', '+'].indexOf(shortName.charAt(0)) >= 0 ? shortName.charAt(0) : ''; var prefix = ['#', '&', '+'].indexOf(shortName.charAt(0)) >= 0 ? shortName.charAt(0) : '';
var title = message.title; var title = parseRichText(message.title);
var number = message.number; var number = message.number;
var pointer = message.pointers[0]; var pointer = message.pointers[0];
var notify = 3; // Default 3 == message var notify = 3; // Default 3 == message
@ -44,6 +86,11 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
notify = message.notify; notify = message.notify;
} }
var rtitle = "";
for (var i = 0; i < title.length; ++i) {
rtitle += title[i].text;
}
/* /*
* Adds a line to this buffer * Adds a line to this buffer
* *
@ -234,6 +281,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
prefix: prefix, prefix: prefix,
number: number, number: number,
title: title, title: title,
rtitle: rtitle,
lines: lines, lines: lines,
clear: clear, clear: clear,
requestedLines: requestedLines, requestedLines: requestedLines,
@ -268,53 +316,11 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var date = message.date; var date = message.date;
var shortTime = $filter('date')(date, 'HH:mm'); var shortTime = $filter('date')(date, 'HH:mm');
function addClasses(textElements) { var prefix = parseRichText(message.prefix);
var typeToClassPrefixFg = {
'option': 'cof-',
'weechat': 'cwf-',
'ext': 'cef-'
};
var typeToClassPrefixBg = {
'option': 'cob-',
'weechat': 'cwb-',
'ext': 'ceb-'
};
textElements.forEach(function(textEl) {
textEl.classes = [];
// foreground color
var prefix = typeToClassPrefixFg[textEl.fgColor.type];
textEl.classes.push(prefix + textEl.fgColor.name);
// background color
prefix = typeToClassPrefixBg[textEl.bgColor.type];
textEl.classes.push(prefix + textEl.bgColor.name);
// attributes
if (textEl.attrs.name !== null) {
textEl.classes.push('coa-' + textEl.attrs.name);
}
var val;
for (var attr in textEl.attrs.override) {
val = textEl.attrs.override[attr];
if (val) {
textEl.classes.push('a-' + attr);
} else {
textEl.classes.push('a-no-' + attr);
}
}
});
}
var prefix = weeChat.Protocol.rawText2Rich(message.prefix);
addClasses(prefix);
var tags_array = message.tags_array; var tags_array = message.tags_array;
var displayed = message.displayed; var displayed = message.displayed;
var highlight = message.highlight; var highlight = message.highlight;
var content = weeChat.Protocol.rawText2Rich(message.message); var content = parseRichText(message.message);
addClasses(content);
if (highlight) { if (highlight) {
prefix.forEach(function(textEl) { prefix.forEach(function(textEl) {

@ -1,7 +1,8 @@
var weechat = angular.module('weechat'); var weechat = angular.module('weechat');
weechat.factory('notifications', ['$rootScope', '$log', 'models', function($rootScope, $log, models) { weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', function($rootScope, $log, models, settings) {
// Ask for permission to display desktop notifications // Ask for permission to display desktop notifications
var notifications = [];
var requestNotificationPermission = function() { var requestNotificationPermission = function() {
// Firefox // Firefox
if (window.Notification) { if (window.Notification) {
@ -52,7 +53,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root
var activeBuffer = models.getActiveBuffer(); var activeBuffer = models.getActiveBuffer();
if (activeBuffer) { if (activeBuffer) {
$rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.title; $rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.rtitle;
} }
}; };
@ -109,6 +110,10 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root
icon: 'assets/img/favicon.png' icon: 'assets/img/favicon.png'
}); });
// Save notification, so we can close all outstanding ones when disconnecting
notification.id = notifications.length;
notifications.push(notification);
// Cancel notification automatically // Cancel notification automatically
var timeout = 15*1000; var timeout = 15*1000;
notification.onshow = function() { notification.onshow = function() {
@ -124,7 +129,12 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root
notification.close(); notification.close();
}; };
if ($rootScope.soundnotification) { // Remove from list of active notifications
notification.onclose = function() {
delete notifications[this.id];
};
if (settings.soundnotification) {
// TODO fill in a sound file // TODO fill in a sound file
var audioFile = "assets/audio/sonar"; var audioFile = "assets/audio/sonar";
var soundHTML = '<audio autoplay="autoplay"><source src="' + audioFile + '.ogg" type="audio/ogg" /><source src="' + audioFile + '.mp3" type="audio/mpeg" /></audio>'; var soundHTML = '<audio autoplay="autoplay"><source src="' + audioFile + '.ogg" type="audio/ogg" /><source src="' + audioFile + '.mp3" type="audio/mpeg" /></audio>';
@ -132,10 +142,20 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root
} }
}; };
var cancelAll = function() {
while (notifications.length > 0) {
var notification = notifications.pop();
if (notification !== undefined) {
notification.close();
}
}
};
return { return {
requestNotificationPermission: requestNotificationPermission, requestNotificationPermission: requestNotificationPermission,
updateTitle: updateTitle, updateTitle: updateTitle,
updateFavico: updateFavico, updateFavico: updateFavico,
createHighlight: createHighlight, createHighlight: createHighlight,
cancelAll: cancelAll,
}; };
}]); }]);

@ -3,7 +3,7 @@
var weechat = angular.module('weechat'); var weechat = angular.module('weechat');
weechat.directive('plugin', ['$rootScope', function($rootScope) { weechat.directive('plugin', ['$rootScope', 'settings', function($rootScope, settings) {
/* /*
* Plugin directive * Plugin directive
* Shows additional plugin content * Shows additional plugin content
@ -20,7 +20,7 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) {
$scope.displayedContent = ""; $scope.displayedContent = "";
// Auto-display embedded content only if it isn't NSFW // Auto-display embedded content only if it isn't NSFW
$scope.plugin.visible = $rootScope.auto_display_embedded_content && !$scope.plugin.nsfw; $scope.plugin.visible = !settings.noembed && !$scope.plugin.nsfw;
// user-accessible hash key that is a valid CSS class name // user-accessible hash key that is a valid CSS class name
$scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_'); $scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_');
@ -47,8 +47,12 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) {
// TODO store the result between channel switches // TODO store the result between channel switches
if ($scope.plugin.content instanceof Function){ if ($scope.plugin.content instanceof Function){
// Don't rerun if the result is already there // Don't rerun if the result is already there
if (embed.innerHTML === "") { if (!embed || embed.innerHTML === "") {
$scope.plugin.content(); // if we're autoshowing, the element doesn't exist yet, and we need
// to do this async (wrapped in a setTimeout)
setTimeout(function() {
$scope.plugin.content();
});
} }
} else { } else {
$scope.displayedContent = $scope.plugin.content; $scope.displayedContent = $scope.plugin.content;

@ -10,15 +10,42 @@ var plugins = angular.module('plugins', []);
/* /*
* Definition of a user provided plugin with sensible default values * Definition of a user provided plugin with sensible default values
* *
* User plugins are created by providing a contentForMessage function * User plugins are created by providing a name and a contentForMessage
* that parses a string and return any additional content. * function that parses a string and returns any additional content.
*/ */
var Plugin = function(contentForMessage) { var Plugin = function(name, contentForMessage) {
return { return {
contentForMessage: contentForMessage, contentForMessage: contentForMessage,
exclusive: false, exclusive: false,
name: "additional content" name: name
};
};
// Regular expression that detects URLs for UrlPlugin
var urlRegexp = /(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g;
/*
* Definition of a user provided plugin that consumes URLs
*
* URL plugins are created by providing a name and a function that
* that parses a URL and returns any additional content.
*/
var UrlPlugin = function(name, urlCallback) {
return {
contentForMessage: function(message) {
var urls = message.match(urlRegexp);
var content = [];
for (var i = 0; urls && i < urls.length; i++) {
var result = urlCallback(urls[i]);
if (result) {
content.push(result);
}
}
return content;
},
exclusive: false,
name: name
}; };
}; };
@ -32,8 +59,6 @@ var Plugin = function(contentForMessage) {
*/ */
plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) { plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) {
var nsfwRegexp = new RegExp('nsfw', 'i');
/* /*
* Defines the plugin manager object * Defines the plugin manager object
*/ */
@ -52,6 +77,8 @@ plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) {
} }
}; };
var nsfwRegexp = new RegExp('nsfw', 'i');
/* /*
* Iterates through all the registered plugins * Iterates through all the registered plugins
* and run their contentForMessage function. * and run their contentForMessage function.
@ -146,23 +173,6 @@ plugins.factory('userPlugins', function() {
document.body.appendChild(script); document.body.appendChild(script);
}; };
var urlRegexp = new RegExp(/(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g);
var urlPlugin = function(callback) {
return function(message) {
var urls = message.match(urlRegexp);
var content = [];
for (var i = 0; urls && i < urls.length; i++) {
var result = callback(urls[i]);
if (result) {
content.push(result);
}
}
return content;
};
};
/* /*
* Spotify Embedded Player * Spotify Embedded Player
* *
@ -170,7 +180,7 @@ plugins.factory('userPlugins', function() {
* *
*/ */
var spotifyPlugin = new Plugin(function(message) { var spotifyPlugin = new Plugin('Spotify track', function(message) {
var content = []; var content = [];
var addMatch = function(match) { var addMatch = function(match) {
for (var i = 0; match && i < match.length; i++) { for (var i = 0; match && i < match.length; i++) {
@ -182,39 +192,29 @@ plugins.factory('userPlugins', function() {
addMatch(message.match(/open.spotify.com\/track\/([a-zA-Z-0-9]{22})/g)); addMatch(message.match(/open.spotify.com\/track\/([a-zA-Z-0-9]{22})/g));
return content; return content;
}); });
spotifyPlugin.name = 'Spotify track';
/* /*
* YouTube Embedded Player * YouTube Embedded Player
* *
* See: https://developers.google.com/youtube/player_parameters * See: https://developers.google.com/youtube/player_parameters
*/ */
var youtubePlugin = new Plugin(function(message) { var youtubePlugin = new UrlPlugin('YouTube video', function(url) {
var regex = /(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/i,
var regExp = /(?:https?:\/\/)?(?:www\.)?(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/gm; match = url.match(regex);
var match = regExp.exec(message);
var content = []; if (match){
var token = match[1],
// iterate over all matches embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0";
while (match !== null){ return '<iframe width="560" height="315" src="'+ embedurl + '" frameborder="0" allowfullscreen frameborder="0"></iframe>';
var token = match[1];
var embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0";
content.push('<iframe width="560" height="315" src="'+ embedurl + '" frameborder="0" allowfullscreen frameborder="0"></iframe>');
// next match
match = regExp.exec(message);
} }
return content;
}); });
youtubePlugin.name = 'YouTube video';
/* /*
* Dailymotion Embedded Player * Dailymotion Embedded Player
* *
* See: http://www.dailymotion.com/doc/api/player.html * See: http://www.dailymotion.com/doc/api/player.html
*/ */
var dailymotionPlugin = new Plugin(function(message) { var dailymotionPlugin = new Plugin('Dailymotion video', function(message) {
var rPath = /dailymotion.com\/.*video\/([^_?# ]+)/; var rPath = /dailymotion.com\/.*video\/([^_?# ]+)/;
var rAnchor = /dailymotion.com\/.*#video=([^_& ]+)/; var rAnchor = /dailymotion.com\/.*#video=([^_& ]+)/;
var rShorten = /dai.ly\/([^_?# ]+)/; var rShorten = /dai.ly\/([^_?# ]+)/;
@ -228,13 +228,11 @@ plugins.factory('userPlugins', function() {
return null; return null;
}); });
dailymotionPlugin.name = 'Dailymotion video';
/* /*
* AlloCine Embedded Player * AlloCine Embedded Player
*/ */
var allocinePlugin = new Plugin(function(message) { var allocinePlugin = new Plugin('AlloCine video', function(message) {
var rVideokast = /allocine.fr\/videokast\/video-(\d+)/; var rVideokast = /allocine.fr\/videokast\/video-(\d+)/;
var rCmedia = /allocine.fr\/.*cmedia=(\d+)/; var rCmedia = /allocine.fr\/.*cmedia=(\d+)/;
@ -247,152 +245,153 @@ plugins.factory('userPlugins', function() {
return null; return null;
}); });
allocinePlugin.name = 'AlloCine video';
/* /*
* Image Preview * Image Preview
*/ */
var imagePlugin = new Plugin( var imagePlugin = new UrlPlugin('image', function(url) {
urlPlugin(function(url) { if (url.match(/\.(png|gif|jpg|jpeg)(:(small|medium|large))?\b/i)) {
var embed = false; /* A fukung.net URL may end by an image extension but is not a direct link. */
// Check the get parameters as well, they might contain an image to load if (url.indexOf("^https?://fukung.net/v/") != -1) {
var segments = url.split(/[?&]/).forEach(function(param) { url = url.replace(/.*\//, "http://media.fukung.net/imgs/");
if (param.match(/\.(png|gif|jpg|jpeg)$/i)) { } else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) {
embed = true; // remove protocol specification to load over https if used by g-b
} url = url.replace(/http:/, "");
}); } else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\/[^?]+$/i)) {
if (embed) { // Dropbox requires a get parameter, dl=1
/* A fukung.net URL may end by an image extension but is not a direct link. */ // TODO strip an existing dl=0 parameter
if (url.indexOf("^https?://fukung.net/v/") != -1) { url = url + "?dl=1";
url = url.replace(/.*\//, "http://media.fukung.net/imgs/");
} else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) {
// remove protocol specification to load over https if used by g-b
url = url.replace(/http:/, "");
} else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\/[^?]+$/i)) {
// Dropbox requires a get parameter, dl=1
url = url + "?dl=1";
}
return '<a target="_blank" href="'+url+'"><img class="embed" src="' + url + '"></a>';
} }
})
); return '<a target="_blank" href="'+url+'"><img class="embed" src="' + url + '"></a>';
imagePlugin.name = 'image'; }
});
/*
* mp4 video Preview
*/
var videoPlugin = new UrlPlugin('video', function(url) {
if (url.match(/\.(mp4|webm|ogv)\b/i)) {
return '<video class="embed" width="560"><source src="'+url+'"></source></video>';
}
});
/* /*
* Cloud Music Embedded Players * Cloud Music Embedded Players
*/ */
var cloudmusicPlugin = new Plugin( var cloudmusicPlugin = new UrlPlugin('cloud music', function(url) {
urlPlugin(function(url) { /* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */
/* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */ if (url.match(/^https?:\/\/soundcloud.com\//)) {
if (url.match(/^https?:\/\/soundcloud.com\//)) { return '<iframe width="100%" height="120" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=' + url + '&amp;color=ff6600&amp;auto_play=false&amp;show_artwork=true"></iframe>';
return '<iframe width="100%" height="120" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=' + url + '&amp;color=ff6600&amp;auto_play=false&amp;show_artwork=true"></iframe>'; }
}
/* MixCloud */ /* MixCloud */
if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) { if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) {
return '<iframe width="480" height="60" src="//www.mixcloud.com/widget/iframe/?feed=' + url + '&mini=1&stylecolor=&hide_artwork=&embed_type=widget_standard&hide_tracklist=1&hide_cover=" frameborder="0"></iframe>'; return '<iframe width="480" height="60" src="//www.mixcloud.com/widget/iframe/?feed=' + url + '&mini=1&stylecolor=&hide_artwork=&embed_type=widget_standard&hide_tracklist=1&hide_cover=" frameborder="0"></iframe>';
} }
}) });
);
cloudmusicPlugin.name = 'cloud music';
/* /*
* Google Maps * Google Maps
*/ */
var googlemapPlugin = new Plugin( var googlemapPlugin = new UrlPlugin('Google Map', function(url) {
urlPlugin(function(url) { if (url.match(/^https?:\/\/maps\.google\./i) || url.match(/^https?:\/\/(?:[\w]+\.)?google\.[\w]+\/maps/i)) {
if (url.match(/^https?:\/\/maps\.google\./i) || url.match(/^https?:\/\/(?:[\w]+\.)?google\.[\w]+\/maps/i)) { return '<iframe width="450" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="' + url + '&output=embed"></iframe>';
return '<iframe width="450" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="' + url + '&output=embed"></iframe>'; }
} });
})
);
googlemapPlugin.name = 'Google Map';
/* /*
* Asciinema plugin * Asciinema plugin
*/ */
var asciinemaPlugin = new Plugin(function(message) { var asciinemaPlugin = new UrlPlugin('ascii cast', function(url) {
var regexp = /^https?:\/\/(?:www\.)?asciinema.org\/a\/(\d+)/i,
match = url.match(regexp);
if (match) {
var id = match[1];
return function() {
var element = this.getElement();
var scriptElem = document.createElement('script');
scriptElem.src = 'https://asciinema.org/a/' + id + '.js';
scriptElem.id = 'asciicast-' + id;
scriptElem.async = true;
element.appendChild(scriptElem);
};
}
});
var regexp = /^https?:\/\/(www\.)?asciinema.org\/a\/(\d+)/; var yrPlugin = new UrlPlugin('meteogram', function(url) {
var match = message.match(regexp); var regexp = /^https?:\/\/(?:www\.)?yr\.no\/(place|stad|sted|sadji|paikka)\/(([^\s.;,(){}<>\/]+\/){3,})/;
var match = url.match(regexp);
if (match) { if (match) {
var id = match[3]; var language = match[1];
return "<script type='text/javascript' src='https://asciinema.org/a/" + id + ".js' id='asciicast-" + id + "' async></script>"; var location = match[2];
var city = match[match.length - 1].slice(0, -1);
url = "http://www.yr.no/" + language + "/" + location + "avansert_meteogram.png";
return "<img src='" + url + "' alt='Meteogram for " + city + "' />";
} }
}); });
asciinemaPlugin.name = "ascii cast";
var yrPlugin = new Plugin(
urlPlugin(function(url) {
var regexp = /^https?:\/\/(?:www\.)?yr\.no\/(place|stad|sted|sadji|paikka)\/(([^\s.;,(){}<>\/]+\/){3,})/;
var match = url.match(regexp);
if (match) {
var language = match[1];
var location = match[2];
var city = match[match.length - 1].slice(0, -1);
url = "http://www.yr.no/" + language + "/" + location + "avansert_meteogram.png";
return "<img src='" + url + "' alt='Meteogram for " + city + "' />";
}
})
);
yrPlugin.name = "meteogram";
// Embed GitHub gists // Embed GitHub gists
var gistPlugin = new Plugin( var gistPlugin = new UrlPlugin('Gist', function(url) {
urlPlugin(function(url) { var regexp = /^https:\/\/gist\.github.com\/[^.?]+/i;
var regexp = /^https:\/\/gist\.github.com\/[^.?]+/i; var match = url.match(regexp);
var match = url.match(regexp); if (match) {
if (match) { // get the URL from the match to trim away pseudo file endings and request parameters
// get the URL from the match to trim away pseudo file endings and request parameters url = match[0] + '.json';
url = match[0] + '.json'; // load gist asynchronously -- return a function here
// load gist asynchronously -- return a function here return function() {
return function() { var element = this.getElement();
var element = this.getElement(); jsonp(url, function(data) {
jsonp(url, function(data) { // Add the gist stylesheet only once
// Add the gist stylesheet only once if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) {
if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) { var stylesheet = '<link rel="stylesheet" href="' + data.stylesheet + '"></link>';
var stylesheet = '<link rel="stylesheet" href="' + data.stylesheet + '"></link>'; document.getElementsByTagName('head')[0].innerHTML += stylesheet;
document.getElementsByTagName('head')[0].innerHTML += stylesheet; }
} element.innerHTML = '<div style="clear:both">' + data.div + '</div>';
element.innerHTML = '<div style="clear:both">' + data.div + '</div>'; });
}); };
}; }
} });
})
); var tweetPlugin = new UrlPlugin('Tweet', function(url) {
gistPlugin.name = 'Gist'; var regexp = /^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/i;
var match = url.match(regexp);
var tweetPlugin = new Plugin( if (match) {
urlPlugin(function(url) { url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2];
var regexp = /^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/i; return function() {
var match = url.match(regexp); var element = this.getElement();
if (match) { jsonp(url, function(data) {
url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2]; // separate the HTML into content and script tag
return function() { var scriptIndex = data.html.indexOf("<script ");
var element = this.getElement(); var content = data.html.substr(0, scriptIndex);
jsonp(url, function(data) { // Set DNT (Do Not Track)
// sepearate the HTML into content and script tag content = content.replace("<blockquote class=\"twitter-tweet\">", "<blockquote class=\"twitter-tweet\" data-dnt=\"true\">");
var scriptIndex = data.html.indexOf("<script "); element.innerHTML = content;
var content = data.html.substr(0, scriptIndex);
// Set DNT (Do Not Track) // The script tag needs to be generated manually or the browser won't load it
content = content.replace("<blockquote class=\"twitter-tweet\">", "<blockquote class=\"twitter-tweet\" data-dnt=\"true\">"); var scriptElem = document.createElement('script');
element.innerHTML = content; // Hardcoding the URL here, I don't suppose it's going to change anytime soon
scriptElem.src = "//platform.twitter.com/widgets.js";
// The script tag needs to be generated manually or the browser won't load it element.appendChild(scriptElem);
var scriptElem = document.createElement('script'); });
// Hardcoding the URL here, I don't suppose it's going to change anytime soon };
scriptElem.src = "//platform.twitter.com/widgets.js"; }
element.appendChild(scriptElem); });
});
}; /*
} * Vine plugin
}) */
); var vinePlugin = new UrlPlugin('Vine', function (url) {
tweetPlugin.name = 'Tweet'; var regexp = /^https?:\/\/(www\.)?vine.co\/v\/([a-zA-Z0-9]+)(\/.*)?/i,
match = url.match(regexp);
if (match) {
var id = match[2], embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1";
return '<iframe class="vine-embed" src="' + embedurl + '" width="600" height="600" frameborder="0"></iframe><script async src="//platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>';
}
});
return { return {
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, tweetPlugin] plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, tweetPlugin, vinePlugin]
}; };

@ -0,0 +1,68 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope) {
var that = this;
this.callbacks = {};
// Define a property for a setting, retrieving it on read
// and writing it to localStorage on write
var defineProperty = function(key) {
Object.defineProperty(that, key, {
enumerable: true,
key: key,
get: function() {
return $store.get(key);
},
set: function(newVal) {
$store.set(key, newVal);
// Call any callbacks
var callbacks = that.callbacks[key];
for (var i = 0; callbacks !== undefined && i < callbacks.length; i++) {
callbacks[i](newVal);
}
// Update the page (might be needed)
setTimeout(function() {
$rootScope.$apply();
}, 0);
}
});
};
// Define properties for all settings
var keys = $store.enumerateKeys();
for (var keyIdx in keys) {
var key = keys[keyIdx];
defineProperty(key);
}
// Add a callback to be called whenever the value is changed
// It's like a free $watch and used to be called the observer
// pattern, but I guess that's too old-school for JS kids :>
this.addCallback = function(key, callback, callNow) {
if (this.callbacks[key] === undefined) {
this.callbacks[key] = [callback];
} else {
this.callbacks[key].push(callback);
}
// call now to emulate $watch behaviour
setTimeout(function() {
callback($store.get(key));
}, 0);
};
this.setDefaults = function(defaults) {
for (var key in defaults) {
// null means the key isn't set
if ($store.get(key) === null) {
this[key] = defaults[key];
}
}
};
return this;
}]);
})();

@ -1,7 +1,7 @@
{ {
"name": "Glowing Bear", "name": "Glowing Bear",
"description": "WeeChat Web frontend", "description": "WeeChat Web frontend",
"version": "0.4.4", "version": "0.4.5",
"manifest_version": 2, "manifest_version": 2,
"icons": { "icons": {
"32": "assets/img/favicon.png", "32": "assets/img/favicon.png",

@ -15,6 +15,15 @@
"name": "The Glowing Bear Authors", "name": "The Glowing Bear Authors",
"url": "https://github.com/glowing-bear" "url": "https://github.com/glowing-bear"
}, },
"permissions": {
"audio-channel-normal" : {
"description" : "Needed to play this app's audio content on the normal channel"
},
"audio-channel-content" : {
"description" : "Needed to play this app's audio content on the content channel"
},
"desktop-notification":{}
},
"default_locale": "en", "default_locale": "en",
"version": "0.4.4" "version": "0.4.5"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,7 +1,7 @@
{ {
"name": "glowing-bear", "name": "glowing-bear",
"private": true, "private": true,
"version": "0.4.4", "version": "0.4.5",
"description": "A web client for Weechat", "description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear", "repository": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3", "license": "GPLv3",
@ -18,7 +18,7 @@
"scripts": { "scripts": {
"postinstall": "bower install", "postinstall": "bower install",
"minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js -c -m --screw-ie8 -o min.js --source-map min.map", "minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/settings.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js -c -m --screw-ie8 -o min.js --source-map min.map",
"prestart": "npm install", "prestart": "npm install",
"start": "http-server -a localhost -p 8000", "start": "http-server -a localhost -p 8000",

@ -7,6 +7,8 @@ module.exports = function(config){
'bower_components/angular/angular.js', 'bower_components/angular/angular.js',
'bower_components/angular-route/angular-route.js', 'bower_components/angular-route/angular-route.js',
'bower_components/angular-mocks/angular-mocks.js', 'bower_components/angular-mocks/angular-mocks.js',
'bower_components/angular-sanitize/angular-sanitize.js',
'bower_components/angular-touch/angular-touch.js',
'js/localstorage.js', 'js/localstorage.js',
'js/weechat.js', 'js/weechat.js',
'js/irc-utils.js', 'js/irc-utils.js',

@ -0,0 +1,65 @@
var weechat = angular.module('weechat');
describe('Filters', function() {
beforeEach(module('weechat'));
/*beforeEach(module(function($provide) {
$provide.value('version', 'TEST_VER');
}));*/
it('has an irclinky filter', inject(function($filter) {
expect($filter('irclinky')).not.toBeNull();
}));
describe('irclinky', function() {
it('should not mess up text', inject(function(irclinkyFilter) {
expect(irclinkyFilter('foo')).toEqual('foo');
}));
it('should linkify IRC channels', inject(function(irclinkyFilter) {
expect(irclinkyFilter('#foo')).toEqual('<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'#foo\'); $scope.$apply();">#foo</a>');
}));
it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) {
expect(irclinkyFilter('<"#foo">')).toEqual('&lt;&quot;<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'#foo&quot;&gt;\'); $scope.$apply();">#foo&quot;&gt;</a>');
}));
});
describe('inlinecolour', function() {
it('should not mess up normal text', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('foo')).toEqual('foo');
expect(inlinecolourFilter('test #foobar baz')).toEqual('test #foobar baz');
}));
it('should detect inline colours in #rrggbb format', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('#123456')).toEqual('#123456 <div class="colourbox" style="background-color:#123456"></div>');
expect(inlinecolourFilter('#aabbcc')).toEqual('#aabbcc <div class="colourbox" style="background-color:#aabbcc"></div>');
}));
it('should not detect inline colours in #rgb format', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('#123')).toEqual('#123');
expect(inlinecolourFilter('#abc')).toEqual('#abc');
}));
it('should detect inline colours in rgb(12,34,56) and rgba(12,34,56,0.78) format', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('rgb(1,2,3)')).toEqual('rgb(1,2,3) <div class="colourbox" style="background-color:rgb(1,2,3)"></div>');
expect(inlinecolourFilter('rgb(1,2,3);')).toEqual('rgb(1,2,3); <div class="colourbox" style="background-color:rgb(1,2,3);"></div>');
expect(inlinecolourFilter('rgba(1,2,3,0.4)')).toEqual('rgba(1,2,3,0.4) <div class="colourbox" style="background-color:rgba(1,2,3,0.4)"></div>');
expect(inlinecolourFilter('rgba(255,123,0,0.5);')).toEqual('rgba(255,123,0,0.5); <div class="colourbox" style="background-color:rgba(255,123,0,0.5);"></div>');
}));
it('should tolerate whitespace in between numbers in rgb/rgba colours', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('rgb( 1\t, 2 , 3 )')).toEqual('rgb( 1\t, 2 , 3 ) <div class="colourbox" style="background-color:rgb( 1\t, 2 , 3 )"></div>');
}));
it('should handle multiple and mixed occurrences of colour values', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('rgb(1,2,3) #123456')).toEqual('rgb(1,2,3) <div class="colourbox" style="background-color:rgb(1,2,3)"></div> #123456 <div class="colourbox" style="background-color:#123456"></div>');
expect(inlinecolourFilter('#f00baa #123456 #234567')).toEqual('#f00baa <div class="colourbox" style="background-color:#f00baa"></div> #123456 <div class="colourbox" style="background-color:#123456"></div> #234567 <div class="colourbox" style="background-color:#234567"></div>');
expect(inlinecolourFilter('rgba(1,2,3,0.4) foorgb(50,100,150)')).toEqual('rgba(1,2,3,0.4) <div class="colourbox" style="background-color:rgba(1,2,3,0.4)"></div> foorgb(50,100,150) <div class="colourbox" style="background-color:rgb(50,100,150)"></div>');
}));
it('should not replace HTML escaped &#123456;', inject(function(inlinecolourFilter) {
expect(inlinecolourFilter('&#123456;')).toEqual('&#123456;');
}));
});
});

@ -47,7 +47,6 @@ describe('filter', function() {
'https://youtu.be/J6vIS8jb6Fs', 'https://youtu.be/J6vIS8jb6Fs',
'http://www.youtube.com/embed/dQw4w9WgXcQ', 'http://www.youtube.com/embed/dQw4w9WgXcQ',
'https://www.youtube.com/embed/dQw4w9WgXcQ', 'https://www.youtube.com/embed/dQw4w9WgXcQ',
'youtu.be/dQw4w9WgXcQ'
], ],
'YouTube video', 'YouTube video',
plugins); plugins);
@ -72,6 +71,16 @@ describe('filter', function() {
plugins); plugins);
})); }));
it('should recognize html5 videos', inject(function(plugins) {
expectTheseMessagesToContain([
'http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4',
'http://www.quirksmode.org/html5/videos/big_buck_bunny.webm',
'http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv',
],
'video',
plugins);
}));
it('should recognize images', inject(function(plugins) { it('should recognize images', inject(function(plugins) {
expectTheseMessagesToContain([ expectTheseMessagesToContain([
'http://i.imgur.com/BTNIDBR.gif', 'http://i.imgur.com/BTNIDBR.gif',
@ -80,7 +89,9 @@ describe('filter', function() {
'https://4z2.de/gb-mobile-new.png', 'https://4z2.de/gb-mobile-new.png',
'http://static.weechat.org/images/screenshots/relay/medium/glowing-bear.png', 'http://static.weechat.org/images/screenshots/relay/medium/glowing-bear.png',
'http://foo.bar/baz.php?img=trololo.png&dummy=yes', 'http://foo.bar/baz.php?img=trololo.png&dummy=yes',
'https://tro.lo.lo/images/rick.png?size=123x45' 'https://tro.lo.lo/images/rick.png?size=123x45',
'https://pbs.twimg.com/media/B66rbCuIMAAxiFF.jpg:large',
'https://pbs.twimg.com/media/B6OZuCYCEAEV8SA.jpg:medium'
], ],
'image', 'image',
plugins); plugins);
@ -136,5 +147,14 @@ describe('filter', function() {
plugins); plugins);
})); }));
it('should recognize vines', inject(function(plugins) {
expectTheseMessagesToContain([
'https://vine.co/v/hWh262H9HM5',
'https://vine.co/v/hWh262H9HM5/embed',
],
'Vine',
plugins);
}));
}); });
}); });

Loading…
Cancel
Save