From c1132ae975b6ccad2ee665eb7fdc8bbd77b6a8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Mon, 25 Jan 2021 17:00:38 +0100 Subject: [PATCH] Implement invite flow in the web portal This allows us to translate the pages using the same tooling and to have consistent theming. --- snikket_web/__init__.py | 2 + snikket_web/invite.py | 158 +++++++++++++ snikket_web/prosodyclient.py | 45 ++++ snikket_web/scss/app.scss | 4 +- snikket_web/scss/invite.scss | 185 +++++++++++++++ snikket_web/static/img/apple/en.svg | 46 ++++ snikket_web/static/img/icons.svg | 15 ++ snikket_web/static/img/illus-empty.svg | 1 + snikket_web/static/img/invite-bg.jpg | Bin 0 -> 38693 bytes snikket_web/static/img/tutorial-scan.png | Bin 0 -> 4644 bytes snikket_web/static/js/invite-magic.js | 55 +++++ snikket_web/static/js/qrcode.min.js | 17 ++ snikket_web/templates/_footer.html | 7 + snikket_web/templates/base.html | 2 +- snikket_web/templates/invite.html | 10 + snikket_web/templates/invite_invalid.html | 12 + snikket_web/templates/invite_register.html | 43 ++++ snikket_web/templates/invite_success.html | 20 ++ snikket_web/templates/invite_view.html | 86 +++++++ snikket_web/templates/library.j2 | 8 +- snikket_web/templates/unauth.html | 8 +- snikket_web/translations/messages.pot | 259 ++++++++++++++++++++- tools/icons.list | 3 + 23 files changed, 963 insertions(+), 23 deletions(-) create mode 100644 snikket_web/invite.py create mode 100644 snikket_web/scss/invite.scss create mode 100644 snikket_web/static/img/apple/en.svg create mode 100644 snikket_web/static/img/illus-empty.svg create mode 100644 snikket_web/static/img/invite-bg.jpg create mode 100644 snikket_web/static/img/tutorial-scan.png create mode 100644 snikket_web/static/js/invite-magic.js create mode 100644 snikket_web/static/js/qrcode.min.js create mode 100644 snikket_web/templates/_footer.html create mode 100644 snikket_web/templates/invite.html create mode 100644 snikket_web/templates/invite_invalid.html create mode 100644 snikket_web/templates/invite_register.html create mode 100644 snikket_web/templates/invite_success.html create mode 100644 snikket_web/templates/invite_view.html 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 0000000000000000000000000000000000000000..1a6480098be71d709731204e1290e6772891979a GIT binary patch literal 38693 zcmeFZcR*9g6F9yJ350+eAaun9DGAa-6%;Q7f)I)!6af)|5J+euV1ZLFARtntg^pe* zL7E&*4n6cN6sZS@D3<$J5zF1#Pw%|*w=d|OpZ5Kge}A8ayti-P%ogP^EQ9An__2U>B0LM>M{o!u#TCJNBmBVbq}QL$o7_CvBnwlTxv7N(#5A{{ z*_+esEv!j2TYGaWdus~_jS~OO3q(gaL@iN8NTEK~3*j9!l1O|EV%A74NJEdhZ++z4J|6kiaXl)P*Ap1mol z2QxCW4rL$CDJ~I}mX%jjR@K!vNE(kfHMewi_nhqgw(r#G3l|3lhlVeWjLux1y)t+8 z+Whr9ckkVQu<&s4(c|YYUcP$$=GV9HenaF!`J z-b|oR^rmCshuH0Yz7d~bMKqmuxo+w18XhMZG%-Pi_0=7PM3+g9hv%_rJ$GEFJL#Fk z9#NeTPjhDis}r_yZmv7&xVmFRpG`r#6Q~DQrMY|hD^h0(R#j~Nkbo`@P4(OKa>I}I ze&d`tB91v}cN}(0t~-^0I>%$0Ej&(FSJmb z`2{hQO%sVPKcnu>!Pr`5echG5o{dfX0BorDsytgF20hlCpFsCb^|V!&tCEP}X*SM9 zPl>wI@D3qPo={coy z(s_OA%r85F_SKGuV0<#YmCb+Q7>}>8I<;wh>mV89yPbcxU}G+J#PK-V==&=E%VjHT z6E6*#8exkYloh>0Pw5kLZeM=Ry4Y7aC@1+M&iZYTvHIF?e!Jca2L14RB2SR+=E~>x z=Jz~ zxsixei3Q2>FHHsKDc^3tl~YYbsomtvu>K4x3FJIp_B~o?!wzo@r1=^@e937KFC6K= zB2)E^_4O$J8tahPmAZOb=U;w3KIy5ZaCF^QBCV4u@j-)GcdE$#xBKh{Jx6=9BqqML zj;U4q_82|XY;+6Hn^8MA!TT|qp?1#d+?XFcZDGP3Y&?}?)uA05wA@mRJJ&$U1DYNTOa(|TgtV~ckKGJ{|jDs54)GvM^L zC#>C_Cps*s0g7H%g6_;VgW2xhS?fyj`tB;O8(~CqnSWduNWZeeha*SY>2jyDO`jPY zS$I?YOTdEv>!$I%FE2j`OWt-mLiD<+_ONQ2K`XByH9>DrV{`AyN7_Y`Ibs=qzk`Yk zr>55~e;vO5D)TrcvDR?6AmjsRww~-;Kf!m~8xgD}#2QMXzEmGeoUFKSvn<|Bw?kK3 zLn(;5=cJuvXWyb{iq2wwQrD)#v|oa{e=J)cU1IuhV*NEyd%l8eH|}m5j`hN*W{ZKf zf5t9}Z@+2xziSa;(0*n-En%~2z$MM* zT8lr6G+aj7598Jx49@Wy`PFM}(1o{0R$f-kds6vruUgAl`a;`{3jsez^G1cUX{EN| z<-6A4>HX7THw&pd&ox$D+K_W5T3w^rAcHm`(7chTb*ehgJLjy44BoH%{6fz|#seNd zZamNEW?AdX{dTS91H@Mirym7OZZS0)5*(`9OgT~-fe%h%dvEjWo(OWaUGAwJ&D=Ch zJk-qM_m0e!BoND))2CV%-@o*lNEh&PzjWskPn==uttbP&zk-&C>An-;-&X9WZWxjr zzH<8V(V|Vn#L`Jmm0~;M9}lfpM*X@qlM%W8RGS3j%S#*^Nt>(PwEiW#QlYAxtp20K z<>ILgZSDQXqQ5D#oNFL_Hx!WE(l*|ZGe_ee((~9&Y2e6DY%z9QIG*Nij_%2eb(ZWu zym{O_T0KvuFeZ2&wd1>$cRM4)+nF{j;?B*R?C!Q^8A!5~uTY8eEka6*ZSYCK=(O%J zml)mMHy;v5A`D75S&1Ipo|4JPvQ<4`w_%_~8MwCg-o`Xs*J5TrpJ@mOO{setZn=%9 zh7ar)e7m-2&_!S2$73(jdrJBoG`DJ4^-1)!1XkzD^lzT?yVY{o&!qF%N$Y_$u2q1x z`6LaY^paWmUoHX?>Jlr+Mb)vU8+mT6QX0>R~C5U4i@f} zCZf7@~kB_!XQ6e=}>)T})T)w6e z#Xk?5D0w>I(bticj`d)ekz{yAhv&Z_ zimWeeEq1+MBg1<7SXjlxFZbNIX?fqEKsCaBmMKzl{3rcRC5_b6y8x*AmWT^T+caQVO_^_Hw8WcHAC0=*|cz9&f~(pT(P&^<*u zK7Oz??}+E|s=g9a`&nI7&UQo9n!1MItujvTx zvZDoSe5U-8l;m=zEsR;K1p98wsmcx=*K`e&c z#_*C!;k+n8yo2s9-+ix35=1!YZn5?>_e^l&@dd7_i9DZFFJEqI4A(9~_lu3B4SVhP z#wEn@lEX-Q<6`5J?Dsn8N{QRUI?`;WOOk>l$2jN)z~#Erjzk{G%GAo#+yp*0?6%N_ zG!i2s?K#daA1S~q2i=dX?%A`)bdRO!j>IT4nw_1UnYo3Tg@p-(FiA>@PY&B_5}&l@ z69s2p5;swhkSy2{PeLe$h40vv?4S$#mof(LaG85y!$;D{wUlsDa)g;1ys(0TKmaof zbMtj3<`yOvb_nlFLd7L`dj5s^AN3a(_ZM_hGIRIe{rpGhB;S+-o*9Riv}0Eym&e@A zi%(wjsk4OGT}hw!wJV8-G)Z}ljfgaZY)VJ$6Fl-1^cOs$uqZ)Xf)pBo`~(%j{R<{x zS7NO6egxNy7t4!-ohLzxw7*~>-e17~k$r?H)QG+F4(=`l-ObrS7wOeLVh2|cVK1%P zM%Y?%?RYekbzC@PRy?~%lQ3H=D-(;zbrI{huxiVV_(aDoJ}EgYp36h%K+2{9NW{+8 z+S=OAhHGML#|<;FGPkof3Fks0wvo1BR#rBV77XIV3 z_AZc67_MFYefjWqeyqg`NK((T591>0aL`Q(+s%U}wA9Guhs8(n zV6=i(ywoE24@MQvTgTLH3#Y^xIKIxWM(bgc`!HEm5vD#eU|t}oE7@X-y$KNZ10BH zfX}XkgjfMD@ozb$xc?F0@5Ybj@?VkuzAwdb+L4d~eS@F8c~AU*?&{wlq<9_zE-yZ5 z$A4t*vwLQUq@TrI$d3mBLSizkhr+te zp5z3iUIFVwxYmq-^)^^HPJ|4?`Zc6Ie5w8vsYglcNOlVl6MZ>OP&Op3MT+{Y9{yRK z%TJ7hFjf#oFCs1;(nkj@)gzGlKBOKOy9@FrEr^7Qym&t!c-DiTm0%Na1D=2lNFW^S z0s;^Vl7R_4$HSH+z=8J}{{>%5iqD2v;SfszxDdk`>;Q2fOo|VXF@P|z{H@!hNK2S= zg9W9?008^rhYv6HVeYR4fIkX9eE7Zg!-qfWU>@xz0O#WW#@`_ZVBKrjUif#L-q!%E z$N->s;P1BZasa+P1c1hTLRey$v>znphK_`J#uvu{SQZF?$`k9|O++lfuwmWk9FTWJ8UayAgx-Y$6xd9ZuzYKcC?x5PzYnOprvY6^2S(|B zh-5GUt>nul0%KTXLs=PO*hojRw-i7CC|IKyaY`wy9K=ZesqAeR0-O+VP`*pKUZOmo zf(J~548$X`aKMoc2vCc_C=K$Pi8aFeO61x9&d|NnLI8B3WONu9AObqO1roxEmk5DO zN5Dcl76O#F0DuN8rVW(l)f8^h7bP6N(YZ#PB}{=%YxGBTS|c z>c)g1CZL;HzRj$3F&>bl;C~-b_f8}EaVQ5C(Fx>!6pIHas98F|cfhQ+5lUGIvPC2m zif=TOLQQ6cHIV*oH?2?HnZBicWI`6budph^}-x zzy@(H2vvau%TlgO2TDvjuCRI9M<4|-cAG9bey3N}W zjs*!_2!wh~IH14^=wShhBi#rugj&GDaUID#XA?VvATJfvgIHLB?<~NBFUY~dGZYRE zB4{NN*&XO~*bazF1V=WONNjd=WJ6QY6)Vwmuyhu{nD=`{gfw5f{u#adv22h7FpvNo zF1nErIZGYT=$wRT44|2^QGkwhC9u(=CMuf^wM@r&s{xL<31~ukq^G-WVBq^ybZj#5 z6s(XfMj+hi=2;sYE>V8Iq9FgcmA9{vo}s##juX>!y_&Q&=K>Kv?KM68dHu`9PZ^yP*QHYsM+7(`Dilp>#WztIzJ5Xgl(Cwe zbH314P>K~6^@6T{CWxkZJBFg06h8JL^+SMe5~AC%0c<7_S`QXd0D*XbPlsITLw{i^ zg>vw|7%`e4lzbEm!PZ&Z0@8f(7`7@hl7AJ|P=HRSsD{yzITemJa7EV%b3-LK0M$my zQPl^&@DByM`Md0G&jd_77EQqub6~5{xlgws5G4SQt3~`@3_vDEK8%?zA_*mqbVpT3 z$SeI5G_#k46aywv8WjP^rFXHY7LE3t z#Y;5)KEn7zM4>a$X+$iWxg-o?KGI->PRGKRXP^tianTnmkm?RAYTCqEDGQk>SN*?9 ze5@gq00vLQ!@)$f1=R>uBOOTqO%#a51OgflsfmR~?5*UI%7an9*Zx&FDJ>lvAsb*& zj5K-U%raC8#6<^$w4tb~lGaUf--VnnkL>?yC8VWgq zRY(vi&_;nOq2*hVaF8?&t4GzH-(>ZXCA)t>WdBV=r$-Oj6)wy2?NB? zqBYGUv?;Rr`dBqoSSUdaQUk)JQ9%%qPy>rxQ5U-bOGNBRTIwK6h|2LKQ0t8Jj7&p^ z6-`;y7}MB&cekKZy zQY18v(m5Gobc0%Pt5m{nNu8a?rWWo?;Du5P!5p-JsyxC$r0S+c5Q#vEzar!7s zTYBO75|_(NG9Akcjdnz*z`gw4?{9cOeYwQmxb)%d@j(y<$oP zS8UjD=3E*))F&Dr&rAU1AtkA+VL=Ntcc?Tv9S@AL0kD+=jRJ`AfKDPGl=VFBJMSJU z1^rhth*7AaGj#k55o>IKLW|*&2Cm$79bJjFe3lsHOoc3xC`5wVH4I>Am^(%J&hP`^ zWF=MUXIw2^qe8Lr_XWB)kp7uOp%}(2Vh*%Gvb-}0%x*HK%_{c#pJ#E7g z*Fxq#7O3yf3jyq%1!Sy~o}<5zj&k&5VyUbiBAvh{WJ7aDr;AjfTJb>GfU6S`Mw5bN zHaX9;IATBg=R`iQ%Vmb@JCDe-Gw4Ck8$_KfPJ6DFkx(9BFybOQpgQ-!389vW&4x-u zxjM-zQmIq7qN%JLF%ThE1~m;OM0B-OkP7N$3!$`FPalUkEnYXhRM0f-FA~DFH2_o; ziz8}v2Y9?+9jO&P1JM2+71@mH_N-i@el0^v1n~tD5KHTy4yYGadir!a99`HM7HtwJ za3_kOWg{Lhp@xgZ=o2{tJ^@i&Ww54Dwzamdj0pn~#D`oG@#)BlLRzGu2e!!#+?1|S zp>aUt&93D`3nri#LM#?br3-%U79}vU)xOnkNf*KerIZ8|3o021r58w8 z5(~-C_bg2tu$_oQXX=AYJe4hmq=`f#8adYkpmjG(dihtbdnpq2`@0_5hH~@k@sR6h)|gU zNp^;7Fx2j@H}+Z-L_UC0%Sxjgb3|$iiWPPmLCN+qH1*xP>%VDuwF(axn%T*WF8Cnn@pUr+XHXWwle0s6kHZliLs8CfJ=1J!Dn_M3Sc|uvmXbrn9 zHMf)GoSJ`JuWf}_togwq#YzuYvwSSlGF)CL86ss=ft|7?nZ9h2Bjm zuup16&pr70Y||@4CtoaZmU7x!sbJ>Qf9A480G~Wo7C5A+q~&BD$z3dPK4Z{OSKTf6 z&R(la*hK7wT1606E|0d~#GTr8LISw~BbQ!W6}>!e9{cmzk}Iq7MKW?S!y(SML$%kN zwY}LSUSy+QSOX$K({AN*+Ckk+>s4DiuPw$-VKK7H2NgAI>zaM#}yIA8U6JcbuoZ!><9)T5Lb(RF!uGSm^o zN+};rushgr_TX%)%86>|^0?(u$NHo4*ZDg(JxwjYp;J=pxIC(=->_)ioMU}kYWdC7 z$<7wkYC|?!=rOeR@VN&Xq*_n+D_?Hno~(u%&suIMdRgI$KV$ISH_Z<6(^gQSSzd+} zFSqE8;Lqs4a)0C?x7A7npzf*n+vRhFv%AZut*_d024&o4CVQhVg${Ijw_M-V-Ta*u zE@{J)`x}mHoja2IqeE2N&(&Sf`((W~mGv8zO&TEFT)m}J-|V=2Q{l6#q6d+VUPX@` zo;+QzMUlJI0Wf(rt!V=b5yH+bF>@ggBc4Q#!Y!X8hE@NtPa;{HxqLVF$m>@J6*@HC zgNN^LxH6)TiTlzsmG{~(#>SUOIAl4Rve_Zb(sla!cGj;GGYh&i)m~Ml=1JuW6^;2v zV}FVYn(;Bze{sxmJh5rnuwehrrw%sBCPFrlU2b^fAId3SyXihXpExwk-?ypkRAVgh zkxu=OXD^;&pM)9((Zp_l+<=k64Lcs`4+AMJY&Y!k>9#x??^$2nOKiI$hLHC(Q z@WT1}wzDVSY-+Zz#;h~T-|td%PaaP3YGtw(H;T=iTo2Ia)kfw7A`I-o`h_%ExfcWP)9x){#TpsXgu4!8Kd* z64tGJdd}L6dqw9o^lTB_I?`PCP3gYYa|_Om%5;wvflzHHlQ-FCysyQKJK zo-sZLRUhU1Q`A;_eP*S3|AU9tlOZvaz^V&fzHTFr6yPp-R*cgVMqx;5&g3*Ikr;VZyGliiOmE9Bym38_^@vjax zd%9cpoy;)bxo+&>T9UZzLXd=ti#Tk8&CpL*JJ(@!rEAKM$<`g~-^Sfye@Y@8KZQ@9 z!mGGU9-h5bwh<o;2BMZO|1t9-Qx)TwWJFmJ}=5iq-Vmq=Kj4y zOe(8x@7>j%I?)>0>XKJsehSWqGC41^oB3FJmnaa|P!@Rs<%*t9b-(s1@KMyseb11I zwlc5X538!msS{>#d>hjVB)lI}fpHD68NM-Er!HGpp+d_tJN@B`mp}cTRq;?~^|2_s z=A-lOrlHoRW%YrDX!`&=0rhy5LS`0cFYcSL+SyCf}rFhu%-Lh$t$}5uL<#l#-g< z4f5Tbg@n3Hu^L>k71u4?ycOkRgEj&!`6lJ=Tz3w9@HLLnoHGxL+|%5OS2LOoei&F5 z{jm9zPheF^V3{Uaxjo+{-w3VQ)Y;VOOvGASHXn7V_G-LreSU9+zPoPtCB_)91;ecB z-W{t!)6jR$q|(byl#R982VPkp=uvaT&_ae*Y%Dq%jW!0JF7=7g3_1u>fV;Isd3Je9 zj4h`-;S`2|H$J`9G<`-aL6vn^*92yI2A*(lrZ>k&cAF;g?6s?+IPt71VK#+A$<>;e z^H&y9(y&oJ_FV5Eugxl${bI5ZSd7j=h1S6b9O9|(${Tlh+%V-<-5&?KX*?6qSXb2| z3SM}4wC0L++Y=u@N0W)Z;Dzz2+0#|sKK9lmKbURt2~39U$oeV}g{9TBMr+l+N96$; z?LjrKE!!rTnBHyfJojpYZtKzsxMT~2I|SpzfL30YsBCbu<#?Cf$|KgrW%>~#`bI%S zAu2Y0TV%Ae(63gBk>=}+&55=(3l2H;U8$L+sE_1}LQx6CX~}%ITZ@75{hBh*==f1l z@xl|eg_{?Rj88TnWfmg4)mr)Pmra=29n732Wl6FNF+#du%1?K=@2Jyfe&)YM@_ z$BS{q*?9YfHS02ClFtBQ(YrZaQGB8?T9fVDzD@(@bc;h&6u6~hqWWLJ!OI}$s zb3R2m8Dvriz{fs`@~LelPoA33cWdS|oY6*?UG62mns{OsFw1;src!l*h)zcjf=ue^ zX~)SB_sQT&eS&YV@3dqwcDlsSQXahs=p5pFy|=%6Wn}W`n6iJ2ePvz1O@X&Eeu5>! zdaKyl%}$Jt!C+L+w2CH|p4T6r3pd8fD@Am>Ow@FDcDE#V9bZ!#I)bCqQLN$fqSn4! z@e_LHp@W5u<=(C+Bhq{`Be*@u$0I|(vf0~OD}T&mzFNOnQaDkI)$0m0F560NndQ&V z*tg%a9=Nyis>OW*5u@7^J3Y33etNxjN6iuIgH&9nZZ>||s^!OhYU?WPDw=t-CmnTJ z#)9PNty&>Pod#P({T|BML77duIh^yMISQer;|u+8g&>5^HSKN+2il%&p41*0+c~|v zr-7U?A$nwb*FE#Xf^RRzGNK{S*7QMLXVOvkq!tZY00uo0rV4{=ee+^@f3kT>0d?TQ z0`L=d__$1%TDBjGcoccrdfO znK$K@!IFhg3Yyf|oyu$Go$7`GDs}z}b+m8g6N)~8MVzjWoP7SMd?w!RYT*l=q)Hqf z4^^{_Rke6DcCoA@@rd>KC<9GJF|FV#;c;?4ZuEX`>=nKkeV(DhwDB=Lp0BlJd8 zc$;j!%S2#9>BKsn)H{JB3d~*B&rZDP%De!whnQA zI+YPpmh8zM8Mq5oMxX#rwMo$8uKb}oQ-9)>DigLJJwxQfi8a^)7c<867B&j6FK(=l zoS2UfgX_N&x98-&i#cPg>h(GmPuCuP(EURsj(`2Jf{L|h)n=DnH|)8=ji)NQZCGhW zPfW(zZn4^;xttgWmFaDlHxXZM(WZIpGnBC>wCz_F_@>@FRbTvWh0=l@d`=f>+3Z>0 zzDCd@ma^tdL1S83eBbUU<`eCVI?5G+wXcbi;(3>Rw}wWqq~P)jT_X`uXhgB8wn;YH zYLOJ8@Nh@ooVtzs;l#+s9R90KlGK5A`TMiCLrcR&GNkDrw|ktSojRH{*%2{Od`Y>z z>*ndQ!pVmYZf%QCkG8s~{1CO47T84?r~CL#EvBwOnDdG%+P6t`KZ1Zxud+W@GMkVa zM1`9MhE}5k@WIt@HYjTsd?vnZ8xu}B+3n9scb8WZG+h^%MB15Ywt*ZX5$LPJ6HyA>WeIc;v~WBdsMrM3qg-8WHKhC#r6{SNzL z$u+GjYh19VwOxc(vD`oL)CA9xV?p1Z}eOa+;q(l*sqFR z6g6&9xUl2kam{|u3xQ>k9o#Kycapqb{I;g<;sj*RAesc~%IuFF-fwqhsPib-zIXD5`A{t)%a!i4iS;zK{;H1TvFLuvmVuPzE{;@?-aa|p^yU~N zy>KDXPQ$e$o5M&ykx!gR>QXzsO7g^Xv`MXpPsm8WHeJ_PTzGz>%~`tR=ialpt6Sj8 zOF8-Q=sK^Og?nthF5QKjX8MEOZF}t9MR!7C)>U?h32LGzI?Z1`++zPQKgvFG3KfAR z-a2Y+|BL+`FN_vw9(pfoJuMO!A(Yv~`Z=|JUZ?btOlityr3ZGJ7(i;g+WFYw_%Q&t zhGL3Dqs3ITS~1+PBae3T?9KR3zY_ryKEZh_vC~t(+ts2k*uB^nZi(UJ?cq}qM>E9a z-9s;x#>l(rbl+gl#Hxn@^HYnb}p9JF1LQLVN=CI1OcGqzSOC|9?m9Dtb?o- z>)`=pLW?}v;Zu7(WM+{K*s3VCt!iVuE=jYgN4pvt4Zp6vYR!qG%AzdwgCQxG^Bz@p zyJj1;1&@u{-|uVg=Gg@f48p`NU1GnfF67Qw;$Gx|khp6|Xve-mB|% zdF@Fxu`Vb@ndIW5w?(-LG=tk+f$jz1v&s2!9ECix_at=EM+TSuy$P-AcKr*d+z;BH zP_?VD^oHASJ`KB%ybKy@j2&v5>uSlQ<9)9j-E-`_$=4=@BUNyBX?}b#@)83sle(b? z`0fr0y^l-==L=!T^@&QFOHFDDU8rfzNo>;#$W+3^?f>aM%{H}=uGbTbd`Yd5GN}}$ z#_o#V_Ry`R`2Oex_~q%Z}%I#n=Fo>5TV`m$NOKlC*Bs#C|2mhB?s2i z%=M@0e!)@86Or?~OmZCAY7>EHq8eZBvTQCfh><@iyH>A@fN)iNef=t34ZCkt4qqC9 zySEanUF(n4JTY8vS94i4V+3%R`XZK^6<^aLI^@(D%T)KeX;qxA5kbUJ@v+bMd>p5I zI~Q(Q0(`Q)p_5yQ>cGnzLot?#$=!a=@}|6jIsNenk$A+21i!k^X|HdLe|aMO<`bVF z7$;g((X8H>!0#sa)C5+I;8=t*1{!dxY+`T1yl%w#%0$RbW4@OCD#`irhmo$z@XP!{ zHGS_;hIVH`9zjrs zBfCt?%7Vp4O?dt^ zA4R7+(k+tm2tWb!0NBEEJ)eVYTEo3&fQsE=+rI0vZAuT^v&X2i4aWJ*eA=b+Q&?(Sjlu3S6Pbab^Rmzo^bKkW6rNg$r|{Dt$IOq+;%6tY4Zcz=}Y( z^-*SZcXPMuz`{WnxH(VA;>a{V+U@~YR8J1?*QFACZPOuuB}F?f7WW1Vs>l1D>1vI2;$+`!(g6rm8l3SsQq?Kkz5 zfBwRxvtwNoaL$k|Z+=uhm28o6c_P?&A@ULw5R1m>!!bemgF1YUf(d~`!N<;Tq2`Q+ zcR7n-o&aEt{Tj$3`nXd26DQos5PJ2{ZL;yO0$f>f5ov+g{4;?AS zz~ozclM-2;EkwgbLJnJv1UcWPWff3bV&KuZmZ~^KzyQC-dofK@WE?94g99zprfIm_ z318Jfiz#A&!!Xb&SK>)W79Nj+Z_L0H3VdwGSTFz`z)XWsm;na{<~)!r57aXX7m>x$ ztcAM38ZFKW_!vhN#32%=g?QgjP17X+8G!!u>kLw|C?&Kxfkankhb9z? zJK$StIM_D|i~azv{9*R?<%6$|&|YNzx$fQedsAP(_qh5F-l$f8A7B0+e$oH(ZyeG0 z1z(9J)O{u*3DtS$`66JenWkk!Kh!V&wlQPiy~qBkURpsH;ajLN%CTNrXV;60imzKlzirIf zuE4#NvU<~0ml}ij^U!eNi;V^Ki-GdF7lv;sa5a7>E5vZQJgW1+U~}GncINdr66!7+ zn+;N!!@tCi4g0JeXH1HCMFIXle@%I9Eu1oK%>3vT{WiO|6Vp&_d{$c>^(EcLM`K z*Ot!P0*eX}0`c4_2>Pn##Q3HCMcJ9Pd9W)JMc&g4(>&M{q`8J5aAw zI6*~NHm%}_PHT)Gnumf6ZOmp&4Q*UxEPm``zNql`H7ZKGAjf&mCdm@h(-1?7_6vgh z^APP<2BC+Rx35;g3F;rWO=C!^A z8^}04aIsE~Iu{eiCv9S{B{zZ@e&Od|6UmObV{8Rry z!HX~)e>->1LIxSf*u(vEr!L{3)3f%1)T=lKA;aL%`(VQ~d3ge#3)|*#o;${HRlDD8K#;=g>NRG9>6Xh z*iC!Fc#yYW;6ANTew(4L_@#IDDQH`B8Tz*hE_{8oV0_5I6#}GA^=C^o-jFqN%OWBi z_>67;$SEHmUf}BVm}(Zju2ndX^C~~oi5m#+UPFH3V&L$Pe1*~X3f&MRVf@euTnNtT ze#Va(;o+vx{9f0q==?I2`L-3goj7~~xAiLF(8!B@g$L(8gBU+xMLwYWbojKWUZ=+l zt!|#7rSZB}Bl}Yi74P}mbH1bfETOz1E2OGNLOE#aE(mi2!>toj`XCIyC8}^z{)E`zcBAM2a9X{{`FhhdYdaPR z8IoskpkI7r@~l0N`v)G@pTo~HPWj~bn#tVNZ}{7foZ_y2J45K>uD;7z{C=WcH0gF@K|Hr9u!%BrLhefEyufB%EcWm9}+s9s+nknwDMF!K*yt-EYoq5Ke8%2^2 zV3ow|_g`S>U8w|QP%!pNgYMD+rIbK27k5lagDwopU!dNydn9I!vV@AKWSmDF8QO@m zfqCW-;b6dNb&6wcn;&kQB6pCGIW@GwPeD?k%6RB9v>}j)n0o#_>FJ)8gzA3xIP7qh z5)9;M80`P*Q3;?DLWP9Fk3FK19^dOE>grmZSAs=yDf{;L_+)itWaO7qkJY`ow`uu~ zN1okdb91?w+|vWjd)Hk53KAy5dFG$Xk_0Fq`vp2o212Zc(+uiU`6MfWWKK_DhYH1? z=9n+YAHdXWs{7k}+S9ZHHbdGf8rzTL1(^+19Aq#Qp7&bUG2SNA`M zUf$l!xA(M2h2O8_Ivd{EK|O$D98eo>eLj?x?U?Yr2A`kqOvdTI9UJWUI)4M>(&nLn zR}ggO{mudcJP()+T^r8ce*Y2T=!l<7%-(3c#%;e3>*Z4QNt??beea`FtMERK>b?Xe z1^t(UDd!Il?kxGMT!|^X^WI7#4Qrpl|5ouPT>UK!nBRL!QQr!7izIIYUZ@bh#~@(8 zcfQDcJN8K7k2q+eUF9R4UF8Dj!9Rl_3;VBKduyele#3o2TO+pa0Zrk3J$GMv_<@Pr z7tgDvWxj!oUsUWbQR`=@{|b?dIy<{WbugBxS8L0{iEZ88x8F!87mdwdYi!mb)4n+N zzPwQKJa*q1g=d{`4m@3PEoAd@^M5Ghoe}KC87}+-z2T}dW!`yUU|{Td2&4SRJd0PP z9QEE;A+H-Y-%j1=pB-YB@iv&ZcZPx9FFvI?ztznbdB7r-_pC{tLv~%|{^~pG zlR1k+Ptu~Vh5WKbN^(bM{-OBkM9R-#>O@ zhBPgo&D7S`h6{=zjm-h~LNCq^Ys8uvth*QX_@Ua_osS<}t)G5XSV1c64g>#d6BjK@t#fE~@q&8g@Z;dSS0^0;w?WL>x@blKDg7=S zZU2Sot&IWh3Pn^Ss^NGcvMB5nFrZSvo2%eg70(*UIl=OOEf^lk} zqJe?;*|i8(9WP_-_O**E6!KCFGqxw_AQl>2gi(P;?A-^aX6&Jr@&8yRCn0Wg#1Qrw z-f(~l3TGUCyg^*IjF8UYJq!9#?xGeM(>LJlxUu3QvYrYd@uG-6fm=I7_x6o@L}&H4*>9y4~9P_A8bl*>>idVHf@z$5T8l$9@pKU@*^bl@2i z+pxJjYxt3r5Om8OnN$`j{oyZbF2T@B$U1LP?xGRanHMpgc^llk#F*Y-esNhIKh+<5 zfk$;0Nv3b_>mpR|lk2$l?zHKS!%uVy?tHILsrYWg;0q`DXBxaaE)EQp+Z;$F4$^xS zmNiyV=q~?k%A=lZmUO-2A6617bW^kB`rYgyjjt|A?Hevr2^nt{%?Hg0TN5gd&4{4i z<2?E#3y`7Qi_FkxXP$OlB=+A5n0f)H5vN@-8aocXPeJAsXgu!soCn4VvKcCBobtnW zSPlw>a_79i*E8Y1b_(r6x=3eCP2)CeLFhXLTMiCD-xv}}2CpnYPp#t;Q#aP)yb3$> zm!&oukcNh|-#$ynwN>zuRYX!s23a=iq{6q^Olg2Y4h$FqOv&>1plKlEe=L6);g~2k z?9kYsL^W&)V+GQf@rX<&^TTAb&E#%5&S{bY!z3>RsrUWHP36@sBW?Jfcy!Knp3%%;V}@AY_oO6W@qpca#n;$>DzC zj&8Ujw1eB3<;n!t3hqnBbPS>iFJU@PPfyP{^@}6(fl znHh)A!HCU+u0Lv+v0;ORx|cEcJT&*x!RHJ@=C$<0-7gr>w;99rFByc2ij$Ak;jpX? z(fAXC=!aC7qDFMBF)}bP*rpMCy7tanJu+8E9dJd|7Bl_3fqBKT zGaAeW7}Dd!_RbGUlEc7mXd|@AT8&k`r6S2i8VBx>InaKt&@g zG4pQbH;Ttvalz&nS5$bZ1=L5s-$H$j^HNs6XuP43mUlp*!8GmdHyY1|7di{nawXrM zY~zNEDP%$L5k;EI@WtoB246Un$~41nkKt%XgrfE5p-&5IXw4{NCPx zbWrgN{X-h{uYych%NLwC^&`ga>^ZPjk!F3T;}1gCh9I-_a;nSlVn=3M*wAYD;6b_e ze(EAIwclUG4E2)+_Zj2j5#x~g;l+Ja7aiHvEovJA7{5xWiTR?le1=-D`Mnj_;o6lS zr#>y8n-!K7uwx?Zr1`~j7heXiG42}U9%vTT@9oHh(I}sqP|32{j?B={;gq6{%o$&v z9XTxO>u)-yxv#2x0=GF*$Jq99#!w*df%_8}2m+9&r1A^U);AL+uj;t+qWWcrzJ+b? zM-J!C<(a?5bAFr2+lYR5qF#~4;=Dz|HY+$_>)x}*JQe7Z+ntA0%*cN%GSuslXKI$W z#qTyYHk|V5glpH{Ag~a@%$@nr{i2R>iu2e5mVtSv%~$7;HNhDe<+hc& z-|&&X7iOserX7gQ-#O%ELImx>)DdmkuobslR*vHuL;SxGLj9@qz1u)rkANvwJK_vj3d%o`jT7ggyAA!9GyNC5N{%Ig)P@k9TPE${vT{8@?z2qWnU zushcf&*C?31}a`cnOL-2tPsS+&bVUI1C7f54Os}VdWCK{fPuJ$t|Subl0WaKUQfMI zkF;|@_PU$Rb<|Jg0*vgK?8bk?CBUx;%y`PvF~xBW#a<*5u){~iLEIAgLdXPAcGC;( ztufIf$v%JcZ|VZh#cuHDcnJXZTkHyt%rw`8tJddL?9W$>05^b4l45KqgLjBd!y=ly*FwHPV}_Cp-^-blJ@|q6h?$Vk_rH z^h(_7SXz43{EW+g;{uNe6k3QuEJO&F!8dV>FY}dy^a+}%-eOO^udllZL9EE+Mnw8n z;#Td3VmE`&1`2rU2}9W{h2_#=LriFz+FPOrzj3Aqagl*ESBx!lB}b@K3SDmMU61R5 zAfHD0|0?TC0HIvJ|A!{cgiJ<=Vy4F6##j<97&BBDOLV_QL}Lj>(ss$(7&~Fe5)pE% z+lE|J_BE94-inkZYd2*qG4}s?r|$Rn|Nnn;=f2I%`##Tep7WgNob#OXc^){dNP-B) z5LLl(PcPw^dfkDW90Q0zC_+hS$R*#!6J;w($Yglk$7=U_X#Zbj1qC!X0-G@+Qbq0{ z&Llx&abxo$6gFg?s`h&m?LOm)-$l-ZbgH1cY|bIW^W*(fx=;q>aATqM0wKrDZ@$swe5C==C^_Z$;Svv~+? zh$x9zLlqa9mw?7bt=By2MQ}(h>0Lw3^#Zvj|K)>F_H;a3uH@Mn`Hz23tduY@}()mu^GT2-LC`K&qJ%oH@n z2$F;h1WLPpBS^^Vc$K<@XgB}h_WxXyfKs59#Nn6(x)ftkgij`GEJPv%N}CYHO%r6c*BL3SXd)Xnskg z^Z$J#wSZf}#m}MuauSe)#;OvwR4sWwH^&K(LQ=+@4AvzGlG)&GOMNTUHebHs}LciC1zt60;#We*|*c$3w|ykOQ+q!#H7&z`Kh*BxB*Auh)1ya|HrpT+qJ5yrjp3)Wxw==xzQVsu4OYyS0st zNgM<8x-E*JvDwXzAIwIBqJ%lj;b4ov z#tE9B#5YZIj6<}$P#hWwuk&h{hT?2C5>UQ&=ca&U55gqy1G>;0S&tu`Ccj6gh=Xl2 zK!uo)1Xjnea|HwxTAT)lohPMpT*-eKaZuQYUFIddSD|c%7w5m6ZuYv9bh90&pn z3`n6v{pWg5Wj`tt7a)imqr`pCf-Z4Oic`5O&VfaCrz~M6`+XpBR*E^z!HXs&j-3l8 z1`6ZA&%=<`MVgbB2=OMn6R^NZMX%Km0hd4X=i*Nbx;b(Te=4yT9>4z$QpM@yp%b!9)<0rwQqr08DyUcLVJNV4+EA@b@_r66iny{adZh$r)ey+HMV1^|d6KW9P|X z7^K*#2$~RfzoxFi-$NkaAmN(eCG?jk4<%P%jK--hc|QPWu#Xy)&$)vbBu0?1_?)*5VWLGo2yA^6j;+sj!`Hqv_{xj{-|oP?z5JcYs~Xr z@nf?b^Qs3EQhXIsWfjEBD02L3B@+wcVxX)&AF>TkNWh@wT*kh@5Pg2SJN@X`qx_^J z?wX$%x?!_t-bki;xmJe$1)`pxy{j6ZUebG@c^!vIMm3A=px)W5Yb0o*{Os-}RVY0< z^U-_a@vZ#SAoUH-1;n#XjUs=np%$L*bve5P-tro1Z}Wx>YOgDnJPnpdkeVQ{gC=Gm zXzVZPKl-i|M!0fu;DSQg+sVKtoiR&P&0A&VbW8IWM9bvh;MJuKM|R3q^}bXlPh+5P z{{@<2qJK3ib?!4`bxEuxI**3|cve^i0E`MDTGsC}vxd7s;xgsIm%U?UHj=9|~9k!>kv*qr;-Vm*Si zwD)3x>VD_UjC;LEOU{~cuXR?dj=Qs%=%q6yH;*VP{eFq&i7NHJlN6h?pimGZ9Ik3Az zSPd&zVUeeQo>SN@vs;04T6=YA-0IKo0|z{IhJTpf>lA$H^?}KWRGRUk{eL`Ob#szd zJ;c|gEzJzGP$LYhXlm`H%Sm<*>5=Nj%8t&r&1q(FRYM`G-^xn9$7WyLedU$SHR8+% z(*D2A=vz`aMBNaqOx(P7f=~fNa`?AfUUPq)uPF4%GT@vjGg){LpZRBVM)8t$#l0=Y zRku;KU#xjor*8gnH|Sw~v0BnB2eXs5F5K5n$g6U4m*a|7?eo81-y5>X@Y0RmyKkGt zKPPkFmuJUr`uKFlH%R968fw=cb1FTyy9PW|hadRBQgVcQ<=KPp6>F%LN2_1=Nc;I? zK9o5Vi`y=2d^38al;5_@9ozmH-MSDU}q`peZTqZ2@ z3d}2en=1z-xSp@TIj}idn6t3MJBjH$i>mTy7$dTYPeiqT?Uvybrcg(_ZMRY`Hw3=K z383GBSjepmSCn=FyLhb+yokmG?i zThS~c4}aV5dNWg_d9Gt$_Dhxj9F1Voqf-tYU)R3E!cuXOzj*B~P#wDWw*(8#qG^YC zyb8(fa3FSd+c0;S3{P%T+V)51K56;LIYKBZyX*9SUUV9wRfuJllC4%4Tx`OLdL@BF z1oTZ8mf0Pv+)53HyT`YDlQBfI*A*4NX8du&spP7#ENvx*a@vNJR`6lMW222=GIMcvrFFV@aXC+O`S1 z6;Kis`tiI-#|N$L3QDw+W|Oh;UpAAif|O z>zN>0ObEWiR%h81{!-`xIj)VLn_)(cSH(A9I>> z?$vpbqQvE7Sx?w9P2h;thlilBXHUOV^;+n-W}4JCDuBjeHdFW5(N@#FRuZdYTtbLV z+hrf>nW!ne*q;lo0Iwj5R3Kl$ZeAEc-9&Fb9sI>A+|lNhnrZ7PjMMeTOA zj|d$c0rF;Mmdl+Pzkn6%`>I8)>I7TW@H!HK5m+O!afhK>dyROlfXSo8FKtujG6FEJ z3*9Hq7=>6jJH53fCAA!hf4oqT_2#|x@4c!V#-Y_#)etOceakn|;?34cI|7C7=>1v! zU3cY|FA61gI;HLX{vXnkb_Rw^`jIkX>!us`UH5*nPiZqaLoEb_KV47)?y8S$MYR(> zB+PGYx>DP~4t9Hx75})a$FtPo-ZR0CaXDC)q?b#p`PTN6vO5>l$-SR@JxiL6P!Ywb zbp+K9k3AsnqTMXi;dPtXHQkomd#)*N7g}hF;UwV6bB6!?lQrrX( zj437}C~=(xKxYM9E}i?d)T3S7_4Mo-Dv6r~FqVa+r6*5M46p8Noc<-@XviUNa=*Lu z^zr#I#q1CRww+2z??FO>YhSOjf{SlTuR?)9@Bj-o&kC=he$hV`=w?zZ6lw~PaB3-kd7=e@F!0G}CR#~e;2x?Ah={{ifvw=z zCm4GOnhsd5_D^>ce!q!$I`f~slRWrKEO-RP!H~qk6YU_OBN1oV@C}aB`Op@xeYj`_{vvTN<*+%F>BOm zIfCc^Eu$8%GgftAIdH1oWps``8N5AUx7478Sm>AFE!Y;^&8}7P)%~9#m^{*sU*58! zu&laVzR1mdH|37hG8<|6;8JkC_u#vHr-jc!dR?p7%MhHdYI?$z{Zg(b8y3U$9zK_f zw}Yg>Nj1BCUPt!za>>8*)mX_@0;@6*d)F{^RN(E>IbprLAITc{(trDrKTy7$->`70 ztTH3!GhiP6{_!KXD^-W_x#HV9sW@fEcy>w^CHY0a3q-vAA1O0pqOF&AP3KUBA-ACg z>49hm2}sT!xBFCyEg#Zo&S288p@~*JmL6SZ&JpwiBkHt)+c}2{>>_Jb#BGo z0x9<1gSt6LcU6Ni%3iI(SjJ#1U#T@areg1`{9y!{s?qsbrJJu6tLc)b32!?x=ze$K zBG4|&Us?fv=c;_6sWxcScSbCk3u)^*`8Z**#&kwZxbCCujx6QZ7>0WBUM z3#1C^r=c-L*^H-hm(D2JNQL>bD*Z1 zl`8xUE!UWFNI&LKR3x1l|EUY^1O@;BU{^I5|$K$h9LCQ7t@pP zSgB_t;EqBH+!@9EKrHgvI}Ir#NPn~IAd%fV0GZ`OdWu)AM+O&;KZZQ3unIr9YKx3Q zDoCoSl}`Y4!L<C=Q zsKoBM2@+W0@f&?JW=}aeHkiC56(wx)Hu*W^)C<(>3>&72m=i#9eP?WNvUAH!eu9nM zI*~oOT;NnaA}7c6y4O%Lv-D{q5iJfW$2u{AoQJCNzase5?mmwTO$%GPAk9v5@2Gnz z)AOz24;-7aTLpo62ARz~BF}K765S;W+yUk=1=Wo)Ax%4CyAG){YAq8jDAznHsFrRd?26_bwP`bC~9*~EPxKVJ;kZ3}ruauN=vBQiCSOIjLx%2MJjeyo4n6#c9Q7@@PsEix0d&{>Sd zQ@>@FPtf}LyIcFtVvDCOQ^7#W0R-|VFz|mF$Pm7TL?DT{7wvbk6i<~e%t-_#;`YOHz3aCTR~ zBFLgp(5B=nhhS8d5RSdlFh}_{pZ7BOO6s`&(KGgz{te5bkniqFe%^nvmfN-HHbTTs z^>59e>3^zU$Nli3)>rQM(0xX2s$GS$UoA7zspm?i6i??9q+UO#=XXx$k6{P%)uecr z|4Dz|_M{HG@PNzQZR#MbCiaS3_1pe(`i19~9Trl9AD_rre?K`nINA5SU1CW8vRb%D z@CL@`;A22qK>lJk-RW|+CjKAmd8(zr!xreya5N!mcNCv#+t=uPAr%_$8OP>E)v{zVs{Xdgz&( zI~{lHGo$ZkwO_R|J`%OEl)Jq6Lzaa9h@txrVn?z*52t4`F&4XD!|&O0K+R(uI1dA_ zYVorjH)~H{K&U#;{)(}TF6&wqIt?P`Yet9hC1TsIj%BTUjZ#9ZBxdS@0S4RlCiklFG>#m~r{v#Y}SuN1j{@6YP)9;wAIFD^es`v3NE zYTmR)JOX_yb9`p%_@eECte<9Wm1**z04 zs@8KI{kfo7C{j0;|I-uiAARykh1d|Z%PpJ?Jd!0fP|eEW=Q3Me~_7yDqP2(MnNucALv)N#j%VC9UUD_70BJZp44W5 zB&VLTqHbSQ|2b?J65X>=Bff!=Cq3s!uUCX!53(NsU58&5`B4eflwk#0J=Tx?)~Af# zDlf6abu_u$18@U?<%Djhr6Fj!as)pF!_Z`G{Ill0VBa+2lRyw%<1ewp^7zQT=bcbh z(Kj|(F&b2^#Sw(b_Mg~lyJTghmtjug!cufU+);QSy$E*1m@cWz2*cwFHK#7H0lSdS z=DE|~c$QhkdU|>nlM~YEt#bG1Z{m&3UZ*uhnwim=Sd5Lcs}jq}Nz%Q$=R!gv_HF#Z zaXNsP&XR`73$`GcVJwQpQ zCuQFVgBz0$#3Cj!)bI_FsGHbp6)!Xw0t`{8I$n>JMWC`w5^R>(5GL zAYcfl)Dsd?qyb;WqT7p$i%V(i5%t$2yvzEV|E$hrM%O}jqZS0Y2;_j87z8YE=IC^Y^K{ zJw7=DE)%i9-F>*Q`FGdAFOG^zZEMtsMVbM#i=lrE44e7A7}?CwGyc2~3 zuH4gEx+$jSRM<$cHsA{>2#2hM-@}%ixLCOJw58_4NqPTCFoKMmRv?0adZnmmDL&^H zY}7po-kYjo*z%S)KUjef8cF|7`@9=NQW$cUZqz$>kG^@ge(^w~_7ASY9K?nF6Uql7 zVzExXX+SNuNK)9?G9TLf>FPpGw0F z1;+>EBq1SeunL6n$IR2f(1^qUyXcdT=XKO@p(ohQ7|W{5&M@-GOoQNV-0IZ?nGV~?XqEkJB)Z$_+u)Ldj@?1o({2xsEJ%aC|WNo zod{cruNuq1Hu%`9HTganVDsFX7`4zv#!S})%rJ3DVzBch@BIjN<*X#-?1JKkQaCCb zI;PC1ohk>=BfKCpYJHKdSX)M^x_@S2Y^p2uXhZWEI-PDLPNoO{oh_lbrGNd5+Km9< zFE;4}PZpLC%lBazu~-)(wPJqxd?pdQj4+>mrh!ZOEprfCkPcv{%=)pyq^5wg_6zOO)uT=*He*KL-0x)ztc z;|uFejAT0t2PP9KfuUm5*aSKbgr2B}e`_I84wJT!JDp{}%@Mz9SL5@eVQ2!sU_D$$4Avz> zi+@wX8u26EEZ;(1-znGaGqYV^7QflQ2p($N&3W@+C>>*Q6iS?F=p+6%RarQz-xT#} zw4tfR?`qA#sme7JXQajPIxZVc+)Ic0v5f)3lB$k181qa(+d56q7qSf}0;IIrWkm(I z!=|WXC#q6#nK}9xFfZhXj%cF3jT!sXi#I*<#Z|Jld(^JwM3HD}i4d#esgZc?DSU=p zWRK}9ET2!;5H3IYRoUm_RT#h1uhX$f!ytq*zLl~qT>C+6E&o|u<{(7D8B){bQ$9ZV zO4$e6a2Z%|I~N>-n=i~%tsnGr4FB<9w#sN#_>{NgQ8tsuiBNswP`ftr>N+YFeuo8HI3<*AZ z(D(7#OvNqB$18MUs*xi15u5yz3vAx;JDt!M2fhxKd57B;-kMD-_i)@sL*m^ZJN{)xhc?}768@f{QJxj z6Szslj%StNZoU}AK42n4kMu7cK4ePOmp!=_hxKS3_EZbUuj(Q<_;<5a_#b)XHg%-A z%qQ!Cwjpdm<~w`9RnlhlgT4b#kY2KXLMizru`BACV@gOJdtciVwJ&C>wUpHcFpiVM zBXTD%7x4Q$#Bbo=i{;M&2S$~$K*Y8@g*#?o$E`FpQ)3m^bNzT8jE8|@+u6UhNCf~< z__2SB@=3M4i4bfC!{*7OAeP`UL@6{yk&q7T`p9pW*BxSH+bj-9`9Jym{}u#S520Cc zqjl=xM*)tD80*yNA&p#7{4pq}ly2%6jZb>>@N+n^NjksbDVVJPyZFA^V1!ve)Xw?* z&J^3Av2pw0rNPdFTPB}l$o9yJ50P-hidj$brLdbuRCS{@);GcO6=U;a3=44F?0!Dq zkc)C)ta_`8dMh<=mLjl7wO=2Z`}ClM6)nJtweQ-&T5MEQ)Cg3@R>UT-$@&6GA#_}! zCn7VMZaOV^9`nBy0XtS3ZCoQlCGwU1K7hAiV+EA;&jqdOLQ3N2?jkFFF>&$HS@$$! z8k)_Hg8tE6Kw*K--Um{LggN=#`S)_ZYIY@7z2POu%|7~>zpXUZa|0Y*=Nw%rUCH?X zpRUK5l?wuiQ{YcypFEQ3L?Q%abNhd(;@blJzqRoz$Mcl^z*|S!#hBk@+voQ-;yD9s zsEfzK5Yn>l01e~{xF<=$vw33=VThp|@;@~N&)nMuizaNdDqIrS10}HRhW@8(pSPrd z%g293+F>6$i+yR=UnZm&^-f!SN@#!9|BZbqLB13}t7 zp)ltk?>8Zp?fZSiCm_BUcHsQO`^<<&gx3@a03T|U5jSQjSx3t796NvZO?SwOU-l z&bh-A^J1-q{#nyiu0bu>7(~J1LT!D*!74OKdvRXU@mo8B8YhBxo;h$U{$3Wkxgd0) zXIn*6anPsv?t<)E&%jV>XGXxsTZbbZbl)#3Q49##GTPck**(-k;QV~b(l3PU8pfj} zg*nE=oSa3&NY8}cNM$QFYDB5N*-_Cg={?nly6E2)uW*+$B+PH$-OawB0coGfWk)sS z-3J0@s>7xg>9cCrU@`BzC`&SgjaSse#WVA_-(X-%&8CMzO+cgToW^idquSg+HTMrs&*`k8jMh-&0Fs!K$TF>I zNcNl(H=CUHd!aYP1jgP?^xI#aVtj_8jyO4;Lh3#0$c{R%V-~6#FX{8HvixukYGN|CO%>ZWcG$RY zQ#!`{XsB$H>}cx)#?%d*eOe`2s!%s73PWJs(ke4^wjKY?UE8WW%kbeB*@qN7W5A+e zx_(;MFxvssiPlMsP-E@$7m^gaA|-8>hcmj~ub5i&%2i%@`0#N~vxusFmVC{S+pT0z zFJ^h%$aX2E)lGG;nx=Vp`IoC!-}gU+42kpkobc}$yXr8m!h=u=i?%k@iA~q`?>{r+ zH?HHnAbV7PQ(m=xO8I4{D{4-DIConM_V|=oQwT|^?z;ZFT@TX_=_-13`!y zm#zMw?5j-;t#A9QAb77+6X0I%zl-b>npnEUiy5|5a=0EjPC1`8J@(<7mL4PFL+1TM zjrb;k;2i^E4aqI`mND&z0wZSrpfqk9-ybpcvW?|?;QF9$Egk1>STNaq>aAMRJu6+D^HjNvk2p(MDqTc9{IdSoho!+u=fdw&d4J%- zUdIgti!5dP7|Dp^Ca8w9)NAaS$E_n#!>~Pbc)ytV{^dhW)upK@X$O0wft-uyv$#DT_<=?))vbFH9QaLCL+0g%=-;+?-(%yO|Gp$gJi9y+kE%%4-b{01 zhD%bhOa8o zETu0qjmV0TdE0^ov3(eYVat7&HYyYtJJ*!waji5$MLULk>CVn0UIRO^uEU3qWZ_sm zn_Qlo&+t_d6>G5lgt^GWi@uhd&-lfJw!b26S5gj)hItyCug|p%mAy7mA)?C=Auh?6 zJX{Um8Z;!Lu|y4cT+#4N&1d}tp`u_u>%;NN_--r1mSNu2?=?i#7T8HNaHU>jy*R$j zQ6XOT)EU(tNfZxuE0~vBQf6Ik;!^$8drYnWq2rb6#QfDhFe9p!eT={$wU7xE zOR;4}ga21%`RLs+cRo*+79JkaAdH-w!F7fOgHtcKc&uT)2{84M#$ct-p=KRk-)jsT zCd6nKN5#czc3+w9M87FGxFk|&L{hjrgdKL@F*d>U2m2e;E+;p&6#v0{(cdyi4BSTv zOYEswPv*Fp*)tyzgKHCdY2Axv&Nfbe6E%k*k%VW0r=h42>y}7VIi$7!*ztSdgCWih zMQF~9!16;0MPs!!ayr_FX)0vZEk!sQQ7WwNW$n!5WnZ~eAfKdw%= zM2yA8@w3&r5F*TVa-}?GYlu_b%AMUc4tGG(DAyIjPYICr8~4LLAnti&i=m*YA`X$T z^9zq}`XYSL_uHcd$<63O=eqNrDQ4Eg#CwR8P7qc=5{WWz)|HDjFzA=GlLFxFOI$&7{R_%0VZO*_4i#%g8P*OLf^K?h!A zF~m*LR$^izD)P;!m+C{X(G8DRD}uaNDE9ik0r@P+O2R=`2t3pAls&nvxZ_9I$o6$Z zt0{TIIyuB`S2k6QbqD(dOBp20RQO%^oz{)RCaD^JX3v&itqrMp1nvxYuOBIKWX*q-OVeg?y ztQ%K-8k&hA*H8`}r=8C;BJNu^^Bkjy!FlWz(1cBXW=!0))V-K)t#$?FLp`P~Q^F!D z&J8scLMgeiGx`cO!C?J58!?Mno51PdtCbLLi5DQ4h@)-+Ty+o7}a+=a9<_*9o@`h;$8? zRVqkJix&PnXE91BR0O;m#)PNmiJdPUe2tgU5)kQY-xf{CLW|4v23+9@jtblv5wUp& zIbE`qK0RCxoaRWn^CtB&Axe~wP$5%-$%|)e)`@mvF=X`y-RrU5mVMI9@{^JD+OCLu z%UQX<8v6-JuYjLvdJ>nKg?}QTi=jv_BAHhf-R<|6XH5p8zf;$Jm%q1iq2jVcBq3M6 zj={pEN|RKi_a0A?g?MzW6C8woN!q@$X}~>5NP1e%$pI}dHCC<8L*vGMWgnVGMGRc6 z58Vasaa!p*8PfZK7}RrWq*43W>^Yx%>u6UDQu-%XEH>z=)xiBh`0X2&GFDFjIp8Hxve?wjTA1Mepa@gz%s%gPQryHWQya%BKOow zFvF3wKk158j;?Uty;bRzSk0&yI~6O7(A$j0arM@|k|?a$(Qdzrzlvh?8a4P^`Ca!* zHUI6KP$^U{JmAnU@in5)%$_k^yk!882lDoQ@dI?5M&6&lUnz1AdlVO&+v#IT+~Y~U z{Q`?~3*l+YzzYe$9vJ(F63*2Lr~3K9LL?X8=CJdN9vF>?;pNEN2?-HtljE~;oKE9& zC3%CO$YSZTm!)#{?-$E^0G`sfwz^4&-**;vTU@_}ffEnnp7N|WtM@U+*wWUkAInqaSjl>su3s3L`U6foGx)P#d z+JT_(*7u5NANjtw5p2Gz5}U|w4QtNH$qkUO-`LnVRof^Bn3nW8{pcxOxAUtOJ(tfk zUCv6Ij6xQypb^K!wvNqSwWN*9=Zq_W(}iRMuuL?pYcAjDsN>1k$rmmUPWhHh#6Q_o zYo=ZY#|1dyigV-qYBv&!@OVjyd{{ukQnie>A!p4HTLGz@+K%o%>80i;6ykcMIiU&Wa&kFYqwr}dT6b$% zQ&RM#=vz4fp6D1Z(gH(25Ubv|F}&mbR*`VK+x%>X0t<7+kY~tiisjy_^K9OEyhA3zn*MQ$dVJ19Td_p!O*l1N>3cnbQ+=9dxA;s zs1mOWFb-H&oN(&Y-64A3%#2xda7h$a00o6KE}W+W{te}-7$&OwuazyUS(H}$ZNAfZ zP{InEu7&6BJw4R?L=oL6?a7=XsW{8?4^*>`M;|fsm39KV=u1`~60xeN{2<$G1Sd4) zLH;l;w7OrGzE=hKfX6pR-H%J&JUB=7S(2;kK0fiJ2TbCSp)EQryiMLItmmsXYNPS7NC0b6XN!o{9zf%OZq$``Mx9-}gwiD6Nz~@gs zep_BgrG_xW;Uo;rSeX9ycs;X8>gpYD7pVsBe&W_MSz4 zzp+$PUY@{a@7vw&-|jQ9U2IqK3uKyMil%)cq)!ARNl2yc*1~jM5ezYf<*AS8G z)7To5R})hUhlSAX{J4T|?hBg6YhJOx`wzhiT!6;v+77W>X1bP+4-F~D`e`sMFr02k^q3Gtn z;i?*n?5%b3h0p_>fDt-TAuK47-;lr%Z_#;m4W5aH)T@~#Exd(LX#jr&%T!|)L8A=0 zf>ExV&6nNA+^8@l8Y2itYEX2&h4m~oPu;pTJvVmC-iD`^s85Up_vY>w5j{MjyPPmY zXvHj_62A`4&fs$y(u4JqqHz)beznv+8g7=~l^O!!80jFR84kC#zKjs2d;v}$VM|Zf zTbBg5*39&4}?|`lvzM?jRyLNy+Ny% z>Bxl5S&yJ2sNztFJ>CZFr|JxCfiY#EKS+uV zqD@Hp278ZAuOFojFE3NabEDgQ_KYAS7LY(YWoEUEJ?{&ZH0QPo3=iHEKG&-D&bx+3 zm0lJODXN>@>Y6bsIEySU+qH7DK1wadvTs_HaWC5)#76{ufqSMbCG*>jCO`hPzzU`h9Egh8CqKRsq?o z(aKECxL7B}+jSyVa?l@)3I>wz>UAR25Qwf!nLP!D0;I*Fr12^uK!vFvP literal 0 HcmV?d00001 diff --git a/snikket_web/static/img/tutorial-scan.png b/snikket_web/static/img/tutorial-scan.png new file mode 100644 index 0000000000000000000000000000000000000000..5682c4e3ff3f4d7adf700308fb07dfaea824de4c GIT binary patch literal 4644 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 ALI3~& literal 0 HcmV?d00001 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