diff --git a/snikket_web/__init__.py b/snikket_web/__init__.py index 260ef81..18493f4 100644 --- a/snikket_web/__init__.py +++ b/snikket_web/__init__.py @@ -216,9 +216,11 @@ def create_app() -> quart.Quart: from .main import bp as main_bp from .user import bp as user_bp from .admin import bp as admin_bp + from .invite import bp as invite_bp app.register_blueprint(main_bp) app.register_blueprint(user_bp, url_prefix="/user") app.register_blueprint(admin_bp, url_prefix="/admin") + app.register_blueprint(invite_bp, url_prefix="/invite") return app diff --git a/snikket_web/invite.py b/snikket_web/invite.py new file mode 100644 index 0000000..1fed37e --- /dev/null +++ b/snikket_web/invite.py @@ -0,0 +1,158 @@ +import pathlib +import typing +import urllib.parse + +import aiohttp + +import quart.flask_patch +from quart import ( + Blueprint, + render_template, + redirect, + url_for, + session as http_session, +) + +import wtforms + +import flask_wtf +from flask_babel import lazy_gettext as _l + +from .infra import client, selected_locale + + +bp = Blueprint("invite", __name__) + + +INVITE_SESSION_JID = "invite-session-jid" + + +# https://play.google.com/store/apps/details?id=org.snikket.android&referrer={uri|urlescape}&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1 + + +def apple_store_badge() -> str: + locale = selected_locale() + filename = "{}.svg".format(locale) + static_path = pathlib.Path(__file__).parent / "static" / "img" / "apple" + if (static_path / filename).exists(): + return url_for("static", filename="img/apple/{}".format(filename)) + return url_for("static", filename="img/apple/en.svg") + + +@bp.context_processor +def context() -> typing.Mapping[str, typing.Any]: + return { + "apple_store_badge": apple_store_badge, + } + + +@bp.route("/") +async def view(id_: str) -> str: + try: + invite = await client.get_public_invite_by_id(id_) + except aiohttp.ClientResponseError as exc: + if exc.status == 404: + # invite expired + return await render_template("invite_invalid.html") + raise + + play_store_url = ( + "https://play.google.com/store/apps/details?" + + urllib.parse.urlencode( + ( + ("id", "org.snikket.android"), + ("referrer", invite.xmpp_uri), + ("pcampaignid", + "pcampaignidMKT-Other-global-all-co-prtnr-py-" + "PartBadge-Mar2515-1"), + ), + ) + ) + apple_store_url = ( + "https://apps.apple.com/us/app/tigase-messenger/id1153516838" + ) + + return await render_template( + "invite_view.html", + invite=invite, + play_store_url=play_store_url, + apple_store_url=apple_store_url, + invite_id=id_, + ) + + +class RegisterForm(flask_wtf.FlaskForm): # type:ignore + localpart = wtforms.StringField( + _l("Username"), + ) + + password = wtforms.PasswordField( + _l("Password"), + ) + + password_confirm = wtforms.PasswordField( + _l("Confirm password"), + validators=[wtforms.validators.InputRequired(), + wtforms.validators.EqualTo( + "password", + _l("The passwords must match") + )] + ) + + action_register = wtforms.SubmitField( + _l("Create account") + ) + + +@bp.route("//register", methods=["GET", "POST"]) +async def register(id_: str) -> typing.Union[str, quart.Response]: + invite = await client.get_public_invite_by_id(id_) + form = RegisterForm() + + if form.validate_on_submit(): + # log the user in? show a guide? no idea. + try: + jid = await client.register_with_token( + username=form.localpart.data, + password=form.password.data, + token=id_, + ) + except aiohttp.ClientResponseError as exc: + if exc.status == 409: + form.localpart.errors.append( + _l("That user name is already taken") + ) + elif exc.status == 403: + form.localpart.errors.append( + _l("Registration was declined for unknown reasons") + ) + elif exc.status == 400: + form.localpart.errors.append( + _l("The user name was not valid") + ) + elif exc.status == 404: + return redirect(url_for(".view", id_=id_)) + else: + raise + else: + http_session[INVITE_SESSION_JID] = jid + return redirect(url_for(".success")) + + return await render_template( + "invite_register.html", + invite=invite, + form=form, + ) + + +@bp.route("/success", methods=["GET", "POST"]) +async def success() -> str: + return await render_template( + "invite_success.html", + jid=http_session.get(INVITE_SESSION_JID, ""), + ) + + +@bp.route("/-") +async def index() -> quart.Response: + return redirect(url_for("index")) diff --git a/snikket_web/prosodyclient.py b/snikket_web/prosodyclient.py index f9c01ed..2c76838 100644 --- a/snikket_web/prosodyclient.py +++ b/snikket_web/prosodyclient.py @@ -114,6 +114,22 @@ class AdminGroupInfo: ) +@dataclasses.dataclass(frozen=True) +class PublicInviteInfo: + inviter: typing.Optional[str] + xmpp_uri: str + + @classmethod + def from_api_response( + cls, + data: typing.Mapping[str, typing.Any], + ) -> "PublicInviteInfo": + return cls( + inviter=data.get("inviter") or None, + xmpp_uri=data["uri"], + ) + + class HTTPSessionManager: def __init__(self, app_context_attribute: str): self._app_context_attribute = app_context_attribute @@ -258,6 +274,9 @@ class ProsodyClient: def _admin_v1_endpoint(self, subpath: str) -> str: return "{}/admin_api{}".format(self._endpoint_base, subpath) + def _public_v1_endpoint(self, subpath: str) -> str: + return "{}/register_api{}".format(self._endpoint_base, subpath) + async def _oauth2_bearer_token(self, session: aiohttp.ClientSession, jid: str, @@ -1053,3 +1072,29 @@ class ProsodyClient: return False scopes = http_session[self.SESSION_CACHED_SCOPE].split() return SCOPE_ADMIN in scopes + + async def get_public_invite_by_id(self, id_: str) -> PublicInviteInfo: + async with self._plain_session as session: + async with session.get(self._public_v1_endpoint( + "/invite/{}".format(id_) + )) as resp: + resp.raise_for_status() + return PublicInviteInfo.from_api_response(await resp.json()) + + async def register_with_token( + self, + token: str, + username: str, + password: str, + ) -> str: + payload = { + "username": username, + "password": password, + "token": token, + } + async with self._plain_session as session: + async with session.post( + self._public_v1_endpoint("/register"), + json=payload) as resp: + resp.raise_for_status() + return (await resp.json())["jid"] diff --git a/snikket_web/scss/app.scss b/snikket_web/scss/app.scss index 926a2e1..cbbf2bd 100644 --- a/snikket_web/scss/app.scss +++ b/snikket_web/scss/app.scss @@ -184,7 +184,7 @@ label.required:after { padding: $w-s4; } -p.form-desc.weak { +p.form-desc.weak, p.field-desc.weak { color: $gray-300; } @@ -1116,7 +1116,7 @@ pre.guru-meditation { } } - p.form-desc.weak { + p.form-desc.weak, p.field-desc.weak { color: $gray-700; } } diff --git a/snikket_web/scss/invite.scss b/snikket_web/scss/invite.scss new file mode 100644 index 0000000..3652f8e --- /dev/null +++ b/snikket_web/scss/invite.scss @@ -0,0 +1,185 @@ +@import "_theme.scss"; + +div.powered-by { + text-align: right; + line-height: 1.5; + + > img { + height: 1.5em; + vertical-align: -0.2em; + margin-left: 0.2em; + } +} + +div.modal { + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + overflow-x: hidden; + overflow-y: auto; + background: rgba(0, 0, 0, 0.5); + z-index: 1024; + width: 100%; + height: 100%; + + > div { + padding: $w-l1; + margin-left: auto; + max-width: 40rem; + margin-right: auto; + + > header { + display: flex; + flex-direction: row; + + > span { + display: inline-block; + flex: 1 1 auto; + } + + > a.button { + flex: 0 0 auto; + } + } + } +} + +div.install-buttons { + display: flex; + flex-direction: column; + align-items: center; + + ul { + display: flex; + flex-direction: row; + list-style-type: none; + margin: $w-l1 0; + padding: 0; + } + + li { + margin: 0; + padding: 0; + } +} + +img.apple { + height: $w-l2; + margin: $w-s2; +} + +img.play { + height: $w-l3; +} + +.tabbox { + display: flex; + flex-direction: column; + margin: $w-l1 0; + + > nav.tabs { + display: flex; + flex-direction: row; + + > a { + display: inline-block; + padding: $w-s2; + border-top-left-radius: $w-s4; + border-top-right-radius: $w-s4; + + &, &:visited { + color: inherit; + text-decoration: underline; + text-decoration-color: $accent-500; + } + + &:hover { + background: $accent-900; + border-color: $accent-800; + color: black; + } + + &.active { + text-decoration: none; + background: linear-gradient(0deg, $accent-600, $accent-700); + color: $accent-200; + + &:hover, &:focus { + background: linear-gradient(0deg, $accent-700, $accent-800); + } + + &:active { + background: $accent-600; + } + } + } + } + + > .tab-pane { + display: none; + padding: 0 $w-0; + background: $accent-900; + + &.active { + display: block; + } + } +} + +.qr { + margin: $w-l1 0; + display: flex; + flex-direction: row; + justify-content: center; + + > img { + padding: $w-0; + background: white; + } +} + +.float-right { + float: right; +} + +#tutorial-scan { + width: $w-l5; + margin: $w-l1; + + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); +} + +div.form.layout-expanded .lwrap { + display: flex; + flex-direction: row; + + input.localpart-magic { + display: inline-block; + width: auto; + flex: 1 0 auto; + } + + span { + display: inline-block; + flex: 0 0 auto; + background: $gray-900; + border: none; + border-bottom: $w-s4 solid $primary-500; + margin-bottom: -$w-s4; + padding: 0 $w-s3; + } +} + +.fullwidth { + width: 100%; +} + +#invite { + background: url('../img/invite-bg.jpg'); + background-attachment: fixed; + background-size: cover; +} diff --git a/snikket_web/static/img/apple/en.svg b/snikket_web/static/img/apple/en.svg new file mode 100644 index 0000000..072b425 --- /dev/null +++ b/snikket_web/static/img/apple/en.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snikket_web/static/img/icons.svg b/snikket_web/static/img/icons.svg index b1de9c5..f4314ee 100644 --- a/snikket_web/static/img/icons.svg +++ b/snikket_web/static/img/icons.svg @@ -27,6 +27,16 @@ licensed under the terms of the Apache 2.0 License --> + + + + + + + + + + @@ -92,4 +102,9 @@ licensed under the terms of the Apache 2.0 License --> + + + + + diff --git a/snikket_web/static/img/illus-empty.svg b/snikket_web/static/img/illus-empty.svg new file mode 100644 index 0000000..7a96302 --- /dev/null +++ b/snikket_web/static/img/illus-empty.svg @@ -0,0 +1 @@ +empty \ No newline at end of file diff --git a/snikket_web/static/img/invite-bg.jpg b/snikket_web/static/img/invite-bg.jpg new file mode 100644 index 0000000..1a64800 Binary files /dev/null and b/snikket_web/static/img/invite-bg.jpg differ diff --git a/snikket_web/static/img/tutorial-scan.png b/snikket_web/static/img/tutorial-scan.png new file mode 100644 index 0000000..5682c4e Binary files /dev/null and b/snikket_web/static/img/tutorial-scan.png differ diff --git a/snikket_web/static/js/invite-magic.js b/snikket_web/static/js/invite-magic.js new file mode 100644 index 0000000..7e87d97 --- /dev/null +++ b/snikket_web/static/js/invite-magic.js @@ -0,0 +1,55 @@ +var open_modal = function(a_el) { + var modal_id = "" + a_el.getAttribute("href").split("#")[1]; + var modal_el = document.getElementById(modal_id); + modal_el.setAttribute("aria-modal", "true"); + modal_el.removeAttribute("aria-hidden"); + modal_el.style.setProperty("display", "block"); +}; + +var close_modal = function(modal_el) { + modal_el.removeAttribute("aria-modal"); + modal_el.setAttribute("aria-hidden", "true"); + modal_el.style.setProperty("display", "none"); +}; + +var find_tabbox_el = function(tab_content_el) { + var current = tab_content_el; + while (current) { + if (current.classList.contains("tabbox")) { + return current; + } + current = current.parentNode; + }; + return null; +}; + +var clear_active_tab = function(tabbox_el) { + var nav_el = tabbox_el.firstChild; + var child = nav_el.firstChild; + while (child) { + child.setAttribute("aria-selected", "false"); + child.classList.remove("active"); + child = child.nextSibling; + } + + var child = nav_el.nextSibling; + while (child) { + if (child.classList.contains("tab-pane")) { + child.classList.remove("active"); + } + child = child.nextSibling; + } +}; + +var select_tab = function(tab_header_el) { + var tab_id = "" + tab_header_el.getAttribute("href").split("#")[1]; + var tab_el = document.getElementById(tab_id); + clear_active_tab(find_tabbox_el(tab_el)); + tab_el.classList.add("active"); + tab_header_el.classList.add("active"); + tab_header_el.setAttribute("aria-selected", "true"); +}; + +var apply_qr_code = function(target_el) { + new QRCode(target_el, target_el.dataset.qrdata); +}; diff --git a/snikket_web/static/js/qrcode.min.js b/snikket_web/static/js/qrcode.min.js new file mode 100644 index 0000000..2ec2f64 --- /dev/null +++ b/snikket_web/static/js/qrcode.min.js @@ -0,0 +1,17 @@ +/* +The MIT License (MIT) +--------------------- +Copyright (c) 2012 davidshimjs + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); diff --git a/snikket_web/templates/_footer.html b/snikket_web/templates/_footer.html new file mode 100644 index 0000000..d24341a --- /dev/null +++ b/snikket_web/templates/_footer.html @@ -0,0 +1,7 @@ +
+
    + {#- -#} +
  • {% trans about_url=url_for('main.about') %}A Snikket service{% endtrans %}
  • + {#- -#} +
+
diff --git a/snikket_web/templates/base.html b/snikket_web/templates/base.html index c0b788e..1c9322b 100644 --- a/snikket_web/templates/base.html +++ b/snikket_web/templates/base.html @@ -16,5 +16,5 @@ - {% block body %}{% endblock %} + {% block body %}{% endblock %} diff --git a/snikket_web/templates/invite.html b/snikket_web/templates/invite.html new file mode 100644 index 0000000..016ca7c --- /dev/null +++ b/snikket_web/templates/invite.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% from "library.j2" import standard_button %} +{% block style %} +{{ super() }} + +{% endblock %} +{% block body %} +
{% block content %}{% endblock %}
+{%- include "_footer.html" -%} +{% endblock %} diff --git a/snikket_web/templates/invite_invalid.html b/snikket_web/templates/invite_invalid.html new file mode 100644 index 0000000..d3961ce --- /dev/null +++ b/snikket_web/templates/invite_invalid.html @@ -0,0 +1,12 @@ +{% extends "invite.html" %} +{% block content %} +
+

{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }}{% endtrans %}

+
{% trans logo_url=url_for("static", filename="img/snikket-logo.svg") %}Powered by Snikket{% endtrans %}
+
+
{% trans %}Invite expired{% endtrans %}
+

{% trans %}Sorry, it looks like this invite code has expired!{% endtrans %}

+
+ Sad person holding empty box +
+{% endblock %} diff --git a/snikket_web/templates/invite_register.html b/snikket_web/templates/invite_register.html new file mode 100644 index 0000000..f58b6fb --- /dev/null +++ b/snikket_web/templates/invite_register.html @@ -0,0 +1,43 @@ +{% extends "invite.html" %} +{% set body_id = "invite" %} +{% from "library.j2" import form_button, render_errors %} +{% block head_lead %} +{% trans site_name=config["SITE_NAME"] %}Register on {{ site_name }} | Snikket{% endtrans %} +{% endblock %} +{% block content %} +
+

{% trans site_name=config["SITE_NAME"] %}Register on {{ site_name }}{% endtrans %}

+
{% trans logo_url=url_for("static", filename="img/snikket-logo.svg") %}Powered by Snikket{% endtrans %}
+

{% trans site_name=config["SITE_NAME"] %}{{ site_name }} is using Snikket - a secure, privacy-friendly chat app.{% endtrans %}

+

{% trans %}Create an account{% endtrans %}

+

{% trans %}Creating an account will allow to communicate with other people using the Snikket app or compatible software. If you already have the app installed, we recommend that you continue the account creation process inside the app by clicking on the button below:{% endtrans %}

+

{% trans %}App already installed?{% endtrans %}

+ {%- call standard_button("exit_to_app", invite.xmpp_uri, class="secondary") -%} + {% trans %}Open the app{% endtrans %} + {%- endcall -%} +

{% trans %}This button works only if you have the app installed already!{% endtrans %}

+

{% trans %}Create an account online{% endtrans %}

+

{% trans %}If you plan to use a legacy XMPP client, you can register an account online and enter your credentials into any XMPP-compatible software.{% endtrans %}

+
+ {{- form.csrf_token -}} + {%- call render_errors(form) %}{% endcall -%} +
+ {{ form.localpart.label }} +
{{ form.localpart(class="localpart-magic") }}@{{ config["SNIKKET_DOMAIN"] }}
+

{% trans %}Choose a username, this will become the first part of your new chat address.{% endtrans %}

+
+
+ {{ form.password.label }} + {{ form.password }} +

{% trans %}Enter a secure password that you do not use anywhere else.{% endtrans %}

+
+
+ {{ form.password_confirm.label }} + {{ form.password_confirm }} +
+
+ {%- call form_button("done", form.action_register, class="primary") -%}{%- endcall -%} +
+
+
+{% endblock %} diff --git a/snikket_web/templates/invite_success.html b/snikket_web/templates/invite_success.html new file mode 100644 index 0000000..caea7f0 --- /dev/null +++ b/snikket_web/templates/invite_success.html @@ -0,0 +1,20 @@ +{% extends "invite.html" %} +{% set body_id = "invite" %} +{% from "library.j2" import form_button, clipboard_button %} +{% block head_lead %} +{% trans site_name=config["SITE_NAME"] %}Successfully registered on {{ site_name }} | Snikket{% endtrans %} +{%- include "copy-snippet.html" -%} +{% endblock %} +{% block content %} +
+

{% trans site_name=config["SITE_NAME"] %}Successfully registered on {{ site_name }}{% endtrans %}

+
{% trans logo_url=url_for("static", filename="img/snikket-logo.svg") %}Powered by Snikket{% endtrans %}
+

{% trans site_name=config["SITE_NAME"], jid=jid %}Congratulations! You successfully registered on {{ site_name }} as {{ jid }}.{% endtrans %}

+ + {%- call clipboard_button(jid, show_label=True) -%} + {% trans %}Copy address{% endtrans %} + {%- endcall -%} +

{% trans %}You can not set up your legacy XMPP client with the above address and the password you chose during registration.{% endtrans %}

+

{% trans %}You can now safely close this page.{% endtrans %}

+
+{% endblock %} diff --git a/snikket_web/templates/invite_view.html b/snikket_web/templates/invite_view.html new file mode 100644 index 0000000..9c48f24 --- /dev/null +++ b/snikket_web/templates/invite_view.html @@ -0,0 +1,86 @@ +{% extends "invite.html" %} +{% set onload = "onload();" %} +{% set body_id = "invite" %} +{% from "library.j2" import action_button %} +{% block head_lead %} +{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }} | Snikket{% endtrans %} + + +{% endblock %} +{% block content %} +
+

{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }}{% endtrans %}

+
{% trans logo_url=url_for("static", filename="img/snikket-logo.svg") %}Powered by Snikket{% endtrans %}
+ {%- if invite.inviter -%} +

{% trans site_name=config["SITE_NAME"], inviter_name=invite.inviter %}You have been invited to chat with {{ inviter_name }} using Snikket, a secure, privacy-friendly chat app on {{ site_name }}.{% endtrans %}

+ {%- else -%} +

{% trans site_name=config["SITE_NAME"] %}You have been invited to chat on {{ site_name }} using Snikket, a secure, privacy-friendly chat app.{% endtrans %}

+ {%- endif -%} +

{% trans %}Get started{% endtrans %}

+

{% trans %}Install the Snikket App on your Android or iOS device.{% endtrans %}

+
+
    +
  • {% trans %}Get it on Google Play{% endtrans %}
  • +
  • {% trans %}Download on the App Store{% endtrans %}
  • +
+ {%- call standard_button("qrcode", "#qr-modal", class="primary", onclick="open_modal(this); return false;") -%} + {% trans %}Not on mobile?{% endtrans %} + {%- endcall -%} +
+

{% trans %}After installation the app should automatically open and prompt you to create an account. If not, simply click the button below.{% endtrans %}

+

{% trans %}App already installed?{% endtrans %}

+ {%- call standard_button("exit_to_app", invite.xmpp_uri, class="secondary") -%} + {% trans %}Open the app{% endtrans %} + {%- endcall -%} +

{% trans %}This button works only if you have the app installed already!{% endtrans %}

+ +

{% trans %}Alternatives{% endtrans %}

+

{% trans register_url=url_for(".register", id_=invite_id) %}You can connect to Snikket using any XMPP-compatible software. If the button above does not work with your app, you may need to register an account manually.{% endtrans %}

+
+ + +{% endblock %} diff --git a/snikket_web/templates/library.j2 b/snikket_web/templates/library.j2 index 866c417..2f7a624 100644 --- a/snikket_web/templates/library.j2 +++ b/snikket_web/templates/library.j2 @@ -24,9 +24,9 @@ {%- if alt %}{{ alt }}{% endif %} {%- endmacro %} -{% macro standard_button(icon_name, href, caller=None, class=None) -%} +{% macro standard_button(icon_name, href, caller=None, class=None, onclick=None) -%} {%- set label = caller() -%} -{% call icon(icon_name) %}{% endcall %}{{ label }} +{% call icon(icon_name) %}{% endcall %}{{ label }} {%- endmacro %} {% macro form_button(icon_name, button_obj, caller=None, class=None) -%} @@ -52,9 +52,9 @@ {%- endmacro %} -{% macro action_button(icon_name, href, caller=None, class=None) -%} +{% macro action_button(icon_name, href, caller=None, class=None, onclick=None) -%} {%- set a11y = caller() -%} -{% call icon(icon_name) %}{% endcall %} +{% call icon(icon_name) %}{% endcall %} {%- endmacro %} {% macro clipboard_button(data, show_label=False, caller=None, class=None) -%} diff --git a/snikket_web/templates/unauth.html b/snikket_web/templates/unauth.html index 26982de..f11159d 100644 --- a/snikket_web/templates/unauth.html +++ b/snikket_web/templates/unauth.html @@ -8,11 +8,5 @@ {% block topbar_right %}{% endblock %}
{% block content %}{% endblock %}
-
-
    - {#- -#} -
  • {% trans about_url=url_for('main.about') %}A Snikket service{% endtrans %}
  • - {#- -#} -
-
+{%- include "_footer.html" -%} {% endblock %} diff --git a/snikket_web/translations/messages.pot b/snikket_web/translations/messages.pot index 113cb75..0ea6bd2 100644 --- a/snikket_web/translations/messages.pot +++ b/snikket_web/translations/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-01-24 11:05+0100\n" +"POT-Creation-Date: 2021-01-25 17:00+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -93,14 +93,42 @@ msgstr "" msgid "Main" msgstr "" -#: snikket_web/main.py:36 -msgid "Address" +#: snikket_web/invite.py:86 +msgid "Username" msgstr "" -#: snikket_web/main.py:41 +#: snikket_web/invite.py:90 snikket_web/main.py:41 msgid "Password" msgstr "" +#: snikket_web/invite.py:94 +msgid "Confirm password" +msgstr "" + +#: snikket_web/invite.py:98 +msgid "The passwords must match" +msgstr "" + +#: snikket_web/invite.py:103 +msgid "Create account" +msgstr "" + +#: snikket_web/invite.py:123 +msgid "That user name is already taken" +msgstr "" + +#: snikket_web/invite.py:127 +msgid "Registration was declined for unknown reasons" +msgstr "" + +#: snikket_web/invite.py:131 +msgid "The user name was not valid" +msgstr "" + +#: snikket_web/main.py:36 +msgid "Address" +msgstr "" + #: snikket_web/main.py:46 msgid "Sign in" msgstr "" @@ -159,6 +187,11 @@ msgstr "" msgid "Incorrect password" msgstr "" +#: snikket_web/templates/_footer.html:4 snikket_web/templates/login.html:36 +#, python-format +msgid "A Snikket service" +msgstr "" + #: snikket_web/templates/about.html:9 msgid "About Snikket" msgstr "" @@ -589,6 +622,219 @@ msgstr "" msgid "The web portal encountered an internal error." msgstr "" +#: snikket_web/templates/invite_invalid.html:4 +#: snikket_web/templates/invite_view.html:11 +#, python-format +msgid "Invite to %(site_name)s" +msgstr "" + +#: snikket_web/templates/invite_invalid.html:5 +#: snikket_web/templates/invite_register.html:9 +#: snikket_web/templates/invite_success.html:10 +#: snikket_web/templates/invite_view.html:12 +#, python-format +msgid "Powered by \"Snikket\"" +msgstr "" + +#: snikket_web/templates/invite_invalid.html:7 +msgid "Invite expired" +msgstr "" + +#: snikket_web/templates/invite_invalid.html:8 +msgid "Sorry, it looks like this invite code has expired!" +msgstr "" + +#: snikket_web/templates/invite_register.html:4 +#, python-format +msgid "Register on %(site_name)s | Snikket" +msgstr "" + +#: snikket_web/templates/invite_register.html:8 +#, python-format +msgid "Register on %(site_name)s" +msgstr "" + +#: snikket_web/templates/invite_register.html:10 +#, python-format +msgid "%(site_name)s is using Snikket - a secure, privacy-friendly chat app." +msgstr "" + +#: snikket_web/templates/invite_register.html:11 +msgid "Create an account" +msgstr "" + +#: snikket_web/templates/invite_register.html:12 +msgid "" +"Creating an account will allow to communicate with other people using the" +" Snikket app or compatible software. If you already have the app " +"installed, we recommend that you continue the account creation process " +"inside the app by clicking on the button below:" +msgstr "" + +#: snikket_web/templates/invite_register.html:13 +#: snikket_web/templates/invite_view.html:30 +msgid "App already installed?" +msgstr "" + +#: snikket_web/templates/invite_register.html:15 +#: snikket_web/templates/invite_view.html:32 +msgid "Open the app" +msgstr "" + +#: snikket_web/templates/invite_register.html:17 +#: snikket_web/templates/invite_view.html:34 +msgid "This button works only if you have the app installed already!" +msgstr "" + +#: snikket_web/templates/invite_register.html:18 +msgid "Create an account online" +msgstr "" + +#: snikket_web/templates/invite_register.html:19 +msgid "" +"If you plan to use a legacy XMPP client, you can register an account " +"online and enter your credentials into any XMPP-compatible software." +msgstr "" + +#: snikket_web/templates/invite_register.html:26 +msgid "" +"Choose a username, this will become the first part of your new chat " +"address." +msgstr "" + +#: snikket_web/templates/invite_register.html:31 +msgid "Enter a secure password that you do not use anywhere else." +msgstr "" + +#: snikket_web/templates/invite_success.html:4 +#, python-format +msgid "Successfully registered on %(site_name)s | Snikket" +msgstr "" + +#: snikket_web/templates/invite_success.html:9 +#, python-format +msgid "Successfully registered on %(site_name)s" +msgstr "" + +#: snikket_web/templates/invite_success.html:11 +#, python-format +msgid "Congratulations! You successfully registered on %(site_name)s as %(jid)s." +msgstr "" + +#: snikket_web/templates/invite_success.html:14 +msgid "Copy address" +msgstr "" + +#: snikket_web/templates/invite_success.html:16 +msgid "" +"You can not set up your legacy XMPP client with the above address and the" +" password you chose during registration." +msgstr "" + +#: snikket_web/templates/invite_success.html:17 +msgid "You can now safely close this page." +msgstr "" + +#: snikket_web/templates/invite_view.html:5 +#, python-format +msgid "Invite to %(site_name)s | Snikket" +msgstr "" + +#: snikket_web/templates/invite_view.html:14 +#, python-format +msgid "" +"You have been invited to chat with %(inviter_name)s using Snikket, a " +"secure, privacy-friendly chat app on %(site_name)s." +msgstr "" + +#: snikket_web/templates/invite_view.html:16 +#, python-format +msgid "" +"You have been invited to chat on %(site_name)s using Snikket, a secure, " +"privacy-friendly chat app." +msgstr "" + +#: snikket_web/templates/invite_view.html:18 +msgid "Get started" +msgstr "" + +#: snikket_web/templates/invite_view.html:19 +msgid "Install the Snikket App on your Android or iOS device." +msgstr "" + +#: snikket_web/templates/invite_view.html:22 +msgid "Get it on Google Play" +msgstr "" + +#: snikket_web/templates/invite_view.html:23 +msgid "Download on the App Store" +msgstr "" + +#: snikket_web/templates/invite_view.html:26 +msgid "Not on mobile?" +msgstr "" + +#: snikket_web/templates/invite_view.html:29 +msgid "" +"After installation the app should automatically open and prompt you to " +"create an account. If not, simply click the button below." +msgstr "" + +#: snikket_web/templates/invite_view.html:36 +msgid "Alternatives" +msgstr "" + +#: snikket_web/templates/invite_view.html:37 +#, python-format +msgid "" +"You can connect to Snikket using any XMPP-compatible software. If the " +"button above does not work with your app, you may need to register an account manually." +msgstr "" + +#: snikket_web/templates/invite_view.html:43 +msgid "Scan invite code" +msgstr "" + +#: snikket_web/templates/invite_view.html:46 +#: snikket_web/templates/invite_view.html:75 +msgid "Close" +msgstr "" + +#: snikket_web/templates/invite_view.html:49 +msgid "" +"You can transfer this invite to your mobile device by scanning a code " +"with your camera. You can use either a QR scanner app or the Snikket app " +"itself." +msgstr "" + +#: snikket_web/templates/invite_view.html:54 +msgid "Using a QR code scanner" +msgstr "" + +#: snikket_web/templates/invite_view.html:56 +msgid "Using the Snikket app" +msgstr "" + +#: snikket_web/templates/invite_view.html:61 +msgid "" +"Use a QR code scanner on your mobile device to scan the code " +"below:" +msgstr "" + +#: snikket_web/templates/invite_view.html:67 +msgid "" +"Install the Snikket app on your mobile device, open it, and tap the " +"'Scan' button at the top." +msgstr "" + +#: snikket_web/templates/invite_view.html:68 +msgid "" +"Your camera will turn on. Point it at the square code below until it is " +"within the highlighted square on your screen, and wait until the app " +"recognises it." +msgstr "" + #: snikket_web/templates/library.j2:18 msgid "Copy link" msgstr "" @@ -609,11 +855,6 @@ msgstr "" msgid "Login failed" msgstr "" -#: snikket_web/templates/login.html:36 snikket_web/templates/unauth.html:14 -#, python-format -msgid "A Snikket service" -msgstr "" - #: snikket_web/templates/user_home.html:3 msgid "Welcome!" msgstr "" diff --git a/tools/icons.list b/tools/icons.list index 3843c6e..a2b9576 100644 --- a/tools/icons.list +++ b/tools/icons.list @@ -3,6 +3,8 @@ action/bug_report:bug_report action/done:done action/logout:logout action/login:login +action/exit_to_app:exit_to_app +communication/qr_code:qrcode communication/vpn_key:passwd content/add_circle_outline:add content/add_link:create_link @@ -16,3 +18,4 @@ navigation/cancel:cancel navigation/more_vert:more social/groups:groups social/group_add:create_group +navigation/close:close