Compare commits

...

279 Commits

Author SHA1 Message Date
Matthew Wild
98e7de3166 Merge pull request #104 from snikket-im/feature/enable-restricted-users
admin: Show restricted user role in the UI
2021-11-09 17:03:08 +00:00
Matthew Wild
61c71b2145 admin: Inline restricted user role name
It was a variable only for the benefit of translators while disabled.
2021-11-09 17:00:59 +00:00
Matthew Wild
6b35e9a259 admin: Show restricted user role in the UI 2021-11-09 16:40:50 +00:00
Matthew Wild
58c2112fec Merge pull request #102 from snikket-im/feature/pin-wtforms
Pin wtforms to 2.x
2021-11-09 10:48:18 +00:00
Jonas Schäfer
c856afee82 Pin wtforms to 2.x
wtforms 3.0 is incompatible with our code. A separate issue will be
filed to address the incompatibilities, but this should be enough to get
working images out of it.

With 3.x, we're seeing:

```
  File "/home/horazont/Projects/python/snikket-web-portal/snikket_web/main.py", line 35, in LoginForm
    address = wtforms.TextField(
AttributeError: module 'wtforms' has no attribute 'TextField'
```

and the portal fails to start.
2021-11-08 18:07:34 +01:00
Matthew Wild
c8356a8e9e Merge pull request #101 from snikket-im/feature/https-qr
Force invite QR code to HTTPS
2021-10-15 14:32:57 +01:00
Jonas Schäfer
0eb464f428 Force invite QR code to HTTPS
We could also do a thing with ProxyFix, but honestly, this should always
be HTTPS.
2021-10-15 15:21:22 +02:00
misiek
2a6ef3c8f1 Translated using Weblate (Polish)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-09-03 15:00:35 +00:00
Matthew Wild
b5d148458a Merge pull request #98 from snikket-im/feature/apple
🎉 Finally set the URL for the App Store
2021-09-02 14:42:02 +01:00
Jonas Schäfer
261758b07a 🎉 Finally set the URL for the App Store
See-Also: https://snikket.org/blog/snikket-ios-public-release/
2021-08-31 17:03:21 +02:00
Jonas Schäfer
ff99c9488a Merge pull request #96 from Zash/invite_success_link
Link to main page after successful registration
2021-08-30 15:47:41 +02:00
Kim Alvefur
fe78631039 Link to main page after successful registration
Someone who registers via the web might also be interested in the web
portal.

Thanks to Jonas and Matthew for feedback on draft commit and help with
whatever this template syntax is.

Also no thanks to git for not actually having draft commits as a concept.
Mercurial is so much nicer to work with.
2021-08-29 15:10:02 +02:00
Kim Alvefur
12ddd288bf Translated using Weblate (Swedish)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-08-01 14:00:44 +00:00
misiek
633fb0d084 Translated using Weblate (Polish)
Currently translated at 97.0% (294 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-08-01 14:00:44 +00:00
uira
f9690063bc Translated using Weblate (Indonesian)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-06-21 23:01:04 +00:00
Jonas Schäfer
65ed50acd3 Merge pull request #90 from snikket-im/hotfix/f-droid-button
Fix F-Droid installation button
2021-06-21 20:23:02 +02:00
Jonas Schäfer
aa04320d70 Fix F-Droid installation button
The button was broken because it was classified as popover, which
means that the JavaScript code will mess with it. In reality,
*that* button was supposed to point at the actual market:// URL.

So we just remove the class and associated data here to fix that.

Fixes #89.
2021-06-20 14:14:30 +02:00
Link Mauve
818d50a1bb Translated using Weblate (French)
Currently translated at 97.6% (296 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-06-19 15:01:05 +00:00
Jonas Schäfer
c7ba7985ea Translated using Weblate (English (United Kingdom))
Currently translated at 40.5% (123 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/en_GB/
2021-06-19 15:01:05 +00:00
Jonas Schäfer
223d127364 Translated using Weblate (English)
Currently translated at 70.2% (213 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/en/
2021-06-19 15:01:04 +00:00
Jonas Schäfer
3a2c4543c4 Translated using Weblate (German)
Currently translated at 100.0% (303 of 303 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-06-19 15:01:04 +00:00
Weblate
c307f057b9 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-06-18 14:20:21 +00:00
Jonas Schäfer
243d5ba236 Merge pull request #86 from snikket-im/feature/show-deleted-circle-members
admin: Show deleted users in circle members
2021-06-18 16:20:18 +02:00
Jonas Schäfer
3d62efccfc admin: Show deleted users in circle members
This helps with removing those users from circles, to avoid them
popping up in peoples roster again.

Even though removal from a circle also only partially works
(roster entries are for instance not cleared), this helps with
ghost users reappearing all the time.
2021-06-18 16:18:22 +02:00
Jonas Schäfer
9d26e39025 Merge pull request #87 from snikket-im/feature/mypy-ci-fix
Install build requirements for mypy CI check
2021-06-18 16:18:12 +02:00
Jonas Schäfer
874f0447ba Install build requirements for mypy CI check
Otherwise, the toml type hints are missing which mypy does not
like.
2021-06-18 16:14:45 +02:00
Jonas Schäfer
0f2127a672 Bring happiness to mypy 2021-06-18 16:11:22 +02:00
Weblate
20d84e7dd1 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-06-18 14:05:55 +00:00
Jonas Schäfer
a02e66023c Fix missing extract_translations run 2021-06-18 16:05:38 +02:00
Jonas Schäfer
e7db9cc772 Fix untranslated table header in admin_edit_circle.html 2021-06-17 17:02:26 +02:00
Jonas Schäfer
e91fb45374 Merge pull request #85 from snikket-im/auto-versioning
Automatically determine version from build info or git
2021-05-31 17:51:59 +02:00
Matthew Wild
531565d55c Automatically determine version from build info or git 2021-05-31 11:20:39 +01:00
Kim Alvefur
c6307619f9 Translated using Weblate (Swedish)
Currently translated at 100.0% (302 of 302 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-05-28 21:00:56 +00:00
Weblate
da2668cbbc Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-05-27 18:31:31 +00:00
Link Mauve
765e3890b4 Translated using Weblate (French)
Currently translated at 97.4% (272 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-05-27 18:31:30 +00:00
Jonas Schäfer
b40a625283 admin: allow disabling display of metrics
This is useful in situations where the admins of the Snikket
server (i.e. those who care for the docker containers) are not the
same people as the people who are admins of the Snikket service
(i.e. those who care for the users).
2021-05-27 17:59:40 +02:00
Jonas Schäfer
8a293985ca Implement system status panel
This offers system metrics and a way to send a broadcast
message to all online or registered users.

Requires prosody-modules cade5dac1003.
2021-05-27 17:21:58 +02:00
Jonas Schäfer
13b2a76c3d Fix mypy errors introduced in b007afc901 2021-05-27 16:33:46 +02:00
Jonas Schäfer
28e01c336d Do not install quart 0.15
As we saw in b007afc901, we cannot
use that version right now.
2021-05-25 18:56:15 +02:00
Jonas Schäfer
5fb0b91178 Bumping version number to 0.2.1 2021-05-22 11:11:50 +02:00
Jonas Schäfer
b007afc901 Revert "Upgrade to quart 0.15"
This reverts commit 486596f89f.
It was discovered that multipart/form-data forms do not work
correctly with Quart 0.15. The upgrade to Quart 0.15 was rushed
and not tested correctly, which I apologize for.

See-Also: https://github.com/pgjones/quart/issues/126
2021-05-22 11:11:16 +02:00
Matthew Wild
7f02746f63 admin: Re-disable 'limited' role (accidentally uncommented in c58ce8450) 2021-05-19 21:59:16 +01:00
Roberto Resoli
f2788aeb36 Translated using Weblate (Italian)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/it/
2021-05-19 15:12:20 +00:00
misiek
536a05b0eb Translated using Weblate (Polish)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-05-19 15:12:20 +00:00
uira
e0226d47e3 Translated using Weblate (Indonesian)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-05-19 15:12:20 +00:00
misiek
0fe10a44ce Translated using Weblate (Polish)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-05-18 13:29:00 +00:00
Jonas Schäfer
e892d81815 Translated using Weblate (German)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-05-18 13:29:00 +00:00
Jonas Schäfer
c58ce8450f Fix type annotations after bumping dependencies 2021-05-18 14:33:06 +02:00
Jonas Schäfer
03573d1f05 Explicitly pass mod_rest JSON as JSON
Otherwise, it can get passed as x-www-form-urlencoded, which
Prosody understandably does not quite like.
2021-05-18 12:35:46 +02:00
Jonas Schäfer
486596f89f Upgrade to quart 0.15 2021-05-18 12:35:31 +02:00
Jonas Schäfer
425b4d4295 Fix dysfunctional password reset button 2021-05-18 12:20:45 +02:00
misiek
87de808046 Translated using Weblate (Polish)
Currently translated at 97.8% (273 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-04-23 20:00:57 +00:00
misiek
05455ac743 Translated using Weblate (Polish)
Currently translated at 96.0% (268 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-04-05 19:01:04 +00:00
Daniel Holmgaard
1e926714cb Translated using Weblate (Danish)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/da/
2021-04-02 19:01:03 +00:00
Tilman Jiménez
e1602f3140 Translated using Weblate (Spanish (Mexico))
Currently translated at 49.1% (137 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/es_MX/
2021-03-31 13:00:53 +00:00
uira
2e89973263 Translated using Weblate (Indonesian)
Currently translated at 99.2% (277 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-03-29 08:00:48 +00:00
Kim Alvefur
a6f1361ddd Translated using Weblate (Swedish)
Currently translated at 100.0% (279 of 279 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-03-27 00:01:12 +00:00
Weblate
552a3bbd41 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-03-25 16:35:52 +00:00
misiek
3f2de1e5bf Translated using Weblate (Polish)
Currently translated at 97.6% (248 of 254 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-03-25 16:35:49 +00:00
Jonas Schäfer
059a10f475 Merge pull request #76 from snikket-im/feature/roles
"Edit user" flow, role management
2021-03-25 17:35:42 +01:00
Jonas Schäfer
a48abacf1d Disable restricted role for now
It is not implemented in snikket-server yet, so we don’t want to
put anything misleading out there.
2021-03-25 17:32:03 +01:00
Jonas Schäfer
ea7ed7c030 Add support for roles
Requires patches to prosody trunk which have been submitted
already (2021-03-22) which introduce the set_roles function on
usermanager.

Fixes #42.
2021-03-25 17:31:56 +01:00
Jonas Schäfer
cca899bd8c Create "Edit user" form
This aggregates the user actions behind a single "edit" button on
the list view, making it less crammed. It also offers the
functionality of actually editing the user, mind.

Also in preparation for #42.

Requires https://hg.prosody.im/prosody-modules/rev/5bc706c2db8f.
2021-03-25 17:31:49 +01:00
Jonas Schäfer
359e6b4ce2 Use tertiary style for "back" buttons
This allows us to have two levels of emphasis for the actual
form buttons and is also in line with the global "Log out"
navigational button.
2021-03-25 17:31:43 +01:00
Jonas Schäfer
6650dd2046 Capitalize App Store in the invite for consistency 2021-03-25 17:28:36 +01:00
Jonas Schäfer
97b4a7be0f Merge pull request #77 from Zash/mod_rest-version-change
Update for switch to datamapper in mod_rest
2021-03-24 09:10:22 +01:00
Kim Alvefur
329916e200 Update for switch to datamapper in mod_rest
mod_rest after the switch to the new util.datamapper in
https://hg.prosody.im/prosody-modules/rev/073f5397c1d2 does not accept
boolean True as value for the xep-0092 'version' field. An empty object
is equivalent and compatible with both previous and future versions.
2021-03-23 21:38:34 +01:00
Kim Alvefur
3571b8909b Translated using Weblate (Swedish)
Currently translated at 100.0% (254 of 254 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-03-23 15:00:36 +00:00
Jonas Schäfer
c6c01b82f5 Merge pull request #74 from snikket-im/feature/invite-from-users-list
Render invite form below user list
2021-03-22 15:30:56 +01:00
Weblate
c4b575f091 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-03-22 14:09:01 +00:00
Jonas Schäfer
fdb55568ec Change problematic "Back" buttons
Fixes #39.
2021-03-22 15:08:33 +01:00
Jonas Schäfer
a9a651be09 Render invite form below user list
Fixes #73.
2021-03-22 15:03:18 +01:00
Kim Alvefur
d2069289b0 Translated using Weblate (Swedish)
Currently translated at 100.0% (252 of 252 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-03-21 16:31:20 +00:00
Link Mauve
552b5d2940 Translated using Weblate (French)
Currently translated at 96.8% (244 of 252 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-03-21 16:31:19 +00:00
Jonas Schäfer
b0f9ae5d57 Translated using Weblate (German)
Currently translated at 100.0% (252 of 252 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-03-21 16:31:19 +00:00
Weblate
dd4a012612 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-03-20 15:59:19 +00:00
Jonas Schäfer
e7aa0a2c45 Fix more dotless strings 2021-03-20 16:44:44 +01:00
Jonas Schäfer
ad229d6700 Use standard error rendering for the login form
This provides a consistent UX.
2021-03-20 16:30:42 +01:00
Jonas Schäfer
b822000f2e Improve install button layout on narrow screens
This allows the button container to add line breaks between the
buttons when necessary.
2021-03-20 16:30:42 +01:00
Jonas Schäfer
a6b67b3fdd Improve install button layout on narrow screens
This allows the button container to add line breaks between the
buttons when necessary.
2021-03-20 16:20:05 +01:00
Jonas Schäfer
885db355ab Add F-Droid download button
Using the mechanism introduced for iOS to describe the multi-step
process.

Fixes #52.
2021-03-20 16:15:20 +01:00
Jonas Schäfer
c3d5b06313 Add multi-step instructions for iOS installation
Fixes #53.
2021-03-20 16:15:07 +01:00
Jonas Schäfer
2dd8838852 Report validity issues of avatar input right away
This helps discovering the actual error message. Thanks @zash.
2021-03-20 15:56:44 +01:00
Jonas Schäfer
5df2c3945a Use browser API to indicate validity state 2021-03-20 14:40:31 +01:00
Jonas Schäfer
3eb8036ebd Implement size checking for the avatar
This checks the avatar size on the client side (if available) and
on the server side against a configuration-defined limit. The
default limit is set to use the same value as in the original
report, as no sensible limit value is known.

Fixes #67.
2021-03-20 12:57:11 +01:00
Jonas Schäfer
02ed390cd2 Fix type annotation 2021-03-20 12:36:06 +01:00
Jonas Schäfer
2506810b90 Return 404 on expired invite URLs 2021-03-19 16:54:40 +01:00
Kim Alvefur
05d1b42dc4 Hint to the browser that the avatar should be PNG
Should result in that the file picker by default only shows PNG files
for selection.
2021-03-17 15:26:30 +01:00
GodGoldfish
5ef5b93eb9 Translated using Weblate (Russian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/ru/
2021-03-12 23:04:55 +00:00
GodGoldfish
0ff6e00e9d Translated using Weblate (French)
Currently translated at 90.1% (220 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-03-12 23:04:54 +00:00
Jonas Schäfer
c04ac4bee0 Make linter happy 2021-03-11 07:32:31 +01:00
Daniel Holmgaard
3e19d42c2a Translated using Weblate (Danish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/da/
2021-03-10 19:03:47 +00:00
Matthew Wild
03732ac06b Fix health check to declare itself text/plain and match Prosody 2021-03-10 14:54:01 +00:00
Matthew Wild
c70228fed7 Add /_health endpoint 2021-03-10 14:22:38 +00:00
Jonas Schäfer
025172592f Improve error handling in Prosody version retrieval 2021-03-09 22:20:37 +01:00
Kim Alvefur
6de1e5313f Add support for displaying prosody version
This only works for authenticated users even in debug mode because
it requires a session with prosody to send the request.

Fixes #66.
2021-03-09 22:08:58 +01:00
Jonas Schäfer
3083c118a3 Add fully translated language codes 2021-03-09 22:03:08 +01:00
Daniel Holmgaard
fa1b13fbdb Translated using Weblate (Danish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/da/
2021-03-06 17:03:39 +00:00
Daniel Holmgaard
ba30d728f4 Translated using Weblate (Danish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/da/
2021-03-03 22:03:08 +00:00
Matthew Wild
af87301fa4 Added translation using Weblate (Danish) 2021-03-01 21:01:54 +00:00
Kim Alvefur
8ee0b0dd30 Translated using Weblate (Swedish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-02-25 16:02:18 +00:00
Roberto Resoli
4a27ef9d72 Translated using Weblate (Italian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/it/
2021-02-25 16:02:18 +00:00
misiek
9e9fdaf8d4 Translated using Weblate (Polish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-02-25 16:02:17 +00:00
uira
bdb186ca81 Translated using Weblate (Indonesian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-02-25 16:02:17 +00:00
Jonas Schäfer
4ca9b82bce Translated using Weblate (German)
Currently translated at 100.0% (244 of 244 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-02-25 16:02:16 +00:00
Jonas Schäfer
6dbe2c2d5e Fix missing type annotation 2021-02-23 21:18:14 +01:00
Jonas Schäfer
e410aedfef Improve logging of rejected IQ calls 2021-02-23 20:21:44 +01:00
Jonas Schäfer
1713da61e7 Fix password change
This fixes a regression introduced in e476d9b7 which caused the
token to be incorrectly used when authenticating with mod_rest for
sending the password change IQ stanza.
2021-02-23 20:20:47 +01:00
Kim Alvefur
53aac690df Add health check to dockerfile 2021-02-23 15:56:19 +01:00
Weblate
5e4009ca11 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-02-23 06:55:53 +00:00
Jonas Schäfer
80860a3ac6 Extract missing strings 2021-02-23 07:55:38 +01:00
Kim Alvefur
e9d479a78b Translated using Weblate (Swedish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/sv/
2021-02-22 23:23:17 +00:00
Jonas Schäfer
aac56f49e9 Added translation using Weblate (Swedish) 2021-02-22 18:12:31 +00:00
Jonas Schäfer
52f0bee006 Use buster-slim as base image
This reduces the overall image size, but more importantly,
deduplicates nicely with the other Snikket images which all use
buster-slim as base.

Fixes #63.
2021-02-22 15:02:14 +01:00
misiek
97c91b432d Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-02-22 11:02:09 +00:00
Jonas Schäfer
60647159f3 Merge pull request #62 from mmigel/patch-2
Update admin_invites.html
2021-02-20 15:55:42 +01:00
Michał Mazur
a21730f136 Update admin_invites.html
A better sounding variant "Expires" in place "Valid until".
2021-02-20 15:53:00 +01:00
Jonas Schäfer
e35ab1b723 Merge pull request #61 from snikket-im/enable-italian-translation
Enable Italian translation by default
2021-02-20 10:17:11 +01:00
Matthew Wild
4de4509fc9 Update __init__.py 2021-02-20 07:07:18 +00:00
misiek
93e3b325b1 Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-02-18 20:01:22 +00:00
Matthew Wild
ceecfc861c docker: allow custom bind interface/port from environment 2021-02-17 13:38:10 +00:00
Link Mauve
2467e73781 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-16 12:02:25 +00:00
Jonas Schäfer
2f34d39a09 Merge pull request #58 from linkmauve/more-translated-titles
Make more titles translatable
2021-02-14 16:09:39 +01:00
Emmanuel Gil Peyrot
de8589923b Make more titles translatable 2021-02-14 13:11:18 +01:00
Tilman Jiménez
db3a1ac22f Translated using Weblate (Spanish (Mexico))
Currently translated at 41.6% (92 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/es_MX/
2021-02-10 17:01:21 +00:00
Jonas Schäfer
b48d130659 Translated using Weblate (German)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-02-10 17:01:20 +00:00
Jonas Schäfer
1aed573eb2 Bump version number for pre-release
(Sigh, I need to get better at this… Or automate it.)
2021-02-09 16:46:18 +01:00
Jonas Schäfer
d4707196ec Include Link header and element in invite response
This allows future App versions to also work with the invite page
without having to screen scrape the content.

Fixes #56 (at least for the portal side of things).
2021-02-09 16:44:50 +01:00
Jonas Schäfer
8a8d4c54bd Collapse the logout button text on narrow displays
This prevents ugly line wraps on long site names
2021-02-09 16:44:50 +01:00
Jonas Schäfer
ab534e3a59 Fix strange 308 error code when using slash-less invite
That seems to be some Quart-internal redirect which isn’t executed
correctly (probably due to our makeshift error handlers). So I
make this a proper redirect instead.
2021-02-09 16:44:50 +01:00
Jonas Schäfer
4c128f1af2 Clarify "Not on mobile" button text
Tester feedback has shown that desktop client users will also
click that button because they are, in fact, not on mobile.

This button speaks more to the users intent (sending the
invitation to the mobile device) after having (hopefully) read
the text above.

Fixes #38.
2021-02-09 16:44:50 +01:00
Jonas Schäfer
8b551a8946 Fix invite page layout after adding support for flashboxes 2021-02-09 16:44:50 +01:00
Tilman Jiménez
182d2301be Translated using Weblate (Spanish (Mexico))
Currently translated at 21.7% (48 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/es_MX/
2021-02-06 18:02:14 +00:00
misiek
6dba5e3a65 Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-02-06 18:02:13 +00:00
Jonas Schäfer
713da89445 Add flash message feedback to all relevant user actions
Fixes #40.
2021-02-06 12:00:55 +01:00
Jonas Schäfer
9876e42fb7 Add support for a flash message sidebar 2021-02-06 12:00:45 +01:00
Jonas Schäfer
8b66c5a063 Add alert role to dynamically added message for a11y 2021-02-06 11:31:55 +01:00
Jonas Schäfer
ddf9f89d77 Remove redundant import 2021-02-06 11:31:51 +01:00
Jonas Schäfer
53e023f9ae Protect against invalid domain on the client side
Here we protect the user from themselves if they accidentally
enter their snikket credentials into the wrong instance by
preventing the form from even being submitted and by showing a
nice error message.
2021-02-06 11:20:05 +01:00
Jonas Schäfer
e4d339627e Protect against incorrect domain name on the server side
Instead of processing the input further and forwarding the
credentials to prosody, we catch the error early on to prevent
having to handle the 400 error code specially and to prevent the
password from spilling in other components.

Fixes #55.
2021-02-06 11:20:05 +01:00
Jonas Schäfer
cd3026911b Added translation using Weblate (Spanish (Mexico)) 2021-02-05 14:30:23 +00:00
GodGoldfish
d7da16f780 Translated using Weblate (Russian)
Currently translated at 55.2% (122 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/ru/
2021-02-04 19:02:06 +00:00
Jonas Schäfer
8ed0fbec25 Translated using Weblate (German)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-02-04 19:02:05 +00:00
Jonas Schäfer
5b812c773d Fix footer on login page 2021-02-04 15:51:43 +01:00
Michał Mazur
fa61ee4e11 Update __init__.py
Wrong Polish language ISO code. That's probably why it doesn't work.
2021-02-04 14:37:51 +01:00
Jonas Schäfer
7402480c62 Allow / suffix on invite URLs
This makes them a bit more clickable in some user agents (think
email, xmpp) which have to rely on parsing to find and highlight
URLs.

Fixes #48.
2021-02-03 19:00:49 +01:00
Jonas Schäfer
a68a469319 Add extended trademark hints to the about page 2021-02-03 18:57:01 +01:00
Jonas Schäfer
961f285fa5 Add trademark info to the footer
Fixes #45.
2021-02-03 18:55:22 +01:00
Jonas Schäfer
7456295cb6 Make title red if running in debug
This (a) helps developers to not accidentally their production
server and (b) deters user from letting it run that way for long.
2021-02-03 18:50:36 +01:00
Jonas Schäfer
96f4b0d4f8 Make version info only available on admin or debug sessions 2021-02-03 18:47:21 +01:00
Jonas Schäfer
245434126e Bump version number for the next release 2021-02-03 18:44:18 +01:00
Jonas Schäfer
725dffc458 Reduce image size by approximately 65% 2021-02-03 18:36:31 +01:00
Jonas Schäfer
22783b837e Update readme screenshot 2021-02-03 18:30:38 +01:00
Jonas Schäfer
ba18fe692f Fix ClientResponseError if a circle has a deleted user
Eventually, we need to clear that on the backend, but for now we
deal with it in the frontend.

Bonus: this also optimises the display of the circle by removing
O(n) backend requests.
2021-02-03 18:25:29 +01:00
riccio
387a989caa Translated using Weblate (English (United Kingdom))
Currently translated at 57.4% (127 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/en_GB/
2021-02-02 21:01:57 +00:00
uira
ffab48cff0 Translated using Weblate (Indonesian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-02-02 21:01:57 +00:00
riccio
17bf7cb140 Translated using Weblate (Italian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/it/
2021-02-02 21:01:57 +00:00
GodGoldfish
408d837a0f Translated using Weblate (Russian)
Currently translated at 42.9% (95 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/ru/
2021-02-02 21:01:57 +00:00
Jonas Schäfer
56e1083ada Translated using Weblate (English)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/en/
2021-02-02 21:01:56 +00:00
Link Mauve
2aa3d629da Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-02 21:01:55 +00:00
Jonas Schäfer
6779341db3 Merge pull request #47 from snikket-im/feature/ci-no-push
Disable GitHub pushing to Docker Hub and include docker build in standard CI testing
2021-02-01 16:20:26 +01:00
Jonas Schäfer
10a0de0637 Add test build to standard CI 2021-02-01 16:17:06 +01:00
Jonas Schäfer
b3185a8d18 Remove build job pushing to docker hub
We don’t want that, Snikket got its own build servers.
2021-02-01 16:16:50 +01:00
Link Mauve
2db6cbe6fd Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-01 15:11:31 +00:00
GodGoldfish
9bc6e0b555 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-01 15:11:31 +00:00
Link Mauve
98a3eeba7c Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-01 15:09:57 +00:00
GodGoldfish
de97b08f01 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-02-01 15:09:57 +00:00
riccio
f2dc970731 Translated using Weblate (Italian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/it/
2021-02-01 11:20:44 +00:00
Jonas Schäfer
2f8e724104 Enable Polish by default 2021-01-31 20:51:03 +01:00
Jonas Schäfer
a3ab537de0 Add more translated languages to the list 2021-01-31 15:29:21 +01:00
Jonas Schäfer
b04c4fa42d Force setting the SNIKKET_DOMAIN as HTTP Host when talking to prosody
This is required if the portal is talking to a Prosody with
multiple HTTP domains and it doesn’t have http_default_host set...
In a true snikket, this doesn’t happen, but we all know...
2021-01-31 15:29:21 +01:00
Jonas Schäfer
078be4ba35 Added translation using Weblate (Italian) 2021-01-31 13:02:47 +00:00
Jonas Schäfer
c1f186a3da Translated using Weblate (German)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-01-31 12:54:49 +00:00
misiek
7aaeb0f368 Translated using Weblate (Polish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-01-31 12:54:48 +00:00
uira
b475e76189 Translated using Weblate (Indonesian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-01-31 12:54:48 +00:00
Jonas Schäfer
8b6f5e8e18 Added translation using Weblate (Russian) 2021-01-31 12:21:39 +00:00
Jonas Schäfer
695ece9165 Add missing file referenced from invites 2021-01-31 12:51:09 +01:00
Jonas Schäfer
7013161872 Add full support for favicon magic in portal even without proxy 2021-01-31 12:51:09 +01:00
Jonas Schäfer
f19270b276 Fix dark mode
Fixes #43.
2021-01-31 10:34:45 +01:00
Link Mauve
cd4955cce8 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:47:38 +00:00
Link Mauve
4d03946a08 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:45:59 +00:00
Link Mauve
60f7d063b6 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:45:01 +00:00
Link Mauve
a57c2c6e20 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:37:20 +00:00
franck
6a5445a525 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:37:20 +00:00
Link Mauve
40a2e6b1b4 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:35:31 +00:00
franck
4b48ebda90 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:35:31 +00:00
Link Mauve
09f498c6b3 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:35:00 +00:00
franck
781bac0ffa Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:35:00 +00:00
Link Mauve
377a28c070 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:34:49 +00:00
franck
0046f2f494 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:34:49 +00:00
Link Mauve
7239b8cbdc Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:34:01 +00:00
franck
0008d0215e Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 13:34:00 +00:00
Weblate
2f69e48ef8 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-30 11:47:45 +00:00
Jonas Schäfer
58a9903a8f Make clipboard button accessible
Without the @href, it is not accessible at all because it is not
considered an active element.
2021-01-30 12:47:37 +01:00
Jonas Schäfer
0dbd8087eb Fix use of incorrect symbol 2021-01-30 12:47:37 +01:00
Jonas Schäfer
ba3440b169 Properly catch 404 on deleted invite
Otherwise, this gives a nasty 500. A proper fix would be to flash
a message and return to the invite list, but that’s more complex
and will be tracked in #40.
2021-01-30 12:47:37 +01:00
Weblate
8a010cf3a3 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-30 10:34:23 +00:00
Jonas Schäfer
e7610928e2 Remove unused template 2021-01-30 11:34:16 +01:00
Weblate
522b65f8ef Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-30 10:15:40 +00:00
Jonas Schäfer
3c90a8ca79 Translated using Weblate (Indonesian)
Currently translated at 93.3% (209 of 224 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-01-30 10:15:40 +00:00
Jonas Schäfer
abd7894b6f Fix nesting mistake 2021-01-30 11:15:29 +01:00
Jonas Schäfer
eaaca163a0 Fix rendering issues in password reset invite pages 2021-01-30 11:05:57 +01:00
Weblate
1c4fa92c97 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-30 09:49:42 +00:00
Link Mauve
42b5f05c8f Translated using Weblate (French)
Currently translated at 91.8% (192 of 209 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-30 09:49:41 +00:00
misiek
59701a4af8 Translated using Weblate (Polish)
Currently translated at 100.0% (209 of 209 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-01-30 09:49:41 +00:00
Jonas Schäfer
5f1d3ba307 Implement password reset flow 2021-01-30 10:49:28 +01:00
uira
985675e012 Translated using Weblate (Indonesian)
Currently translated at 100.0% (209 of 209 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-01-30 09:07:21 +00:00
Weblate
f372b31b9d Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-29 14:59:13 +00:00
Jonas Schäfer
9612926230 Address a bunch of accessibility issues
- Add missing labels for form fields
- Improve contrast slightly
- Remove incorrect aria-label= and title= attributes
- Use fieldset for radio button groups
2021-01-29 15:58:54 +01:00
Jonas Schäfer
57adf0c679 Fix dark mode
It was broken after the welcome card refactor.
2021-01-29 15:58:54 +01:00
Matthew Wild
c28e98ec18 Set appropriate interface and port in docker 2021-01-29 14:03:26 +00:00
Matthew Wild
f27c86e29a Initialize required environment variables appropriately 2021-01-29 13:59:30 +00:00
Matthew Wild
ec94a47c8c Update to use logo including text for invite pages 2021-01-29 08:48:13 +00:00
Weblate
362587d852 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-29 08:32:44 +00:00
Jonas Schäfer
25f161d2b0 Make boxes friendlier 2021-01-29 09:32:11 +01:00
Jonas Schäfer
e5c5bbfbb4 Fix broken form validation for main circle 2021-01-29 09:32:06 +01:00
Jonas Schäfer
4044e857bc Disable iOS app reference by default
The app is not published yet, so we hide it by default. But we
allow allow to set an environment variable to show the button with
an appropriate link.
2021-01-29 09:32:01 +01:00
Jonas Schäfer
f4348600e2 Minor tweak to user profile home card
This aligns the buttons of the two cards and also the aspect
ratio becomes a tad nicer.
2021-01-28 20:10:18 +01:00
Jonas Schäfer
028f9b35a6 Fix leak of internal circle name in tooltips 2021-01-28 20:01:22 +01:00
Weblate
50d5fd21e0 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-28 17:55:46 +00:00
pep
a75bf333bd Translated using Weblate (Japanese)
Currently translated at 64.1% (129 of 201 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/ja/
2021-01-28 17:55:45 +00:00
pep
47975cb1a6 Translated using Weblate (French)
Currently translated at 100.0% (201 of 201 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-28 17:55:45 +00:00
misiek
2ae9425b6a Translated using Weblate (Polish)
Currently translated at 100.0% (201 of 201 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-01-28 17:55:45 +00:00
uira
00b4d528c9 Translated using Weblate (Indonesian)
Currently translated at 100.0% (201 of 201 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-01-28 17:55:45 +00:00
Jonas Schäfer
e58a3176ac Also revamp the admin index 2021-01-28 18:55:22 +01:00
Jonas Schäfer
205b0173a7 Revamp the welcome screen for a hopefully final iteration 2021-01-28 18:53:03 +01:00
Jonas Schäfer
6235231db5 Add avatar to edit profile form 2021-01-28 18:53:03 +01:00
Jonas Schäfer
af61705482 Iterate further on the welcome page 2021-01-28 18:53:03 +01:00
Jonas Schäfer
46c7b3be11 Relayout the "welcome" screens again
Grouping the buttons together with the text for easier grasping
what they relate to and moving them to the bottom.
2021-01-28 18:53:03 +01:00
Jonas Schäfer
547286b2e3 Fix path to snikket logo 2021-01-28 18:53:03 +01:00
Jonas Schäfer
f68db94d91 Redo the welcome navigation
Tester feedback suggested that it looks really content-heavy,
more like blogpost previews or something like that. We now move
to a clear, recognizable "action button with description" style
using the already prominent primary colour.
2021-01-28 18:53:03 +01:00
Jonas Schäfer
16da296f79 Tweak colours slightly
- Make them warmer overall
- Fix tone in the upper yellows
2021-01-28 18:53:03 +01:00
Jonas Schäfer
d5ad562d2c Revamp rendering of invitation type to better accomodate roster invites 2021-01-28 16:15:46 +01:00
Jonas Schäfer
c2126419d4 Add support for tooltips in CSS 2021-01-28 16:15:38 +01:00
Jonas Schäfer
7028770f40 Add background to invalid invite page 2021-01-28 15:00:57 +01:00
Matthew Wild
350fd29622 templates: Render ?roster invites as type 'Contact' 2021-01-28 11:04:22 +00:00
Matthew Wild
a6aef681a7 Add support for 'roster' invite type 2021-01-28 11:00:05 +00:00
Weblate
5256872646 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-27 16:17:11 +00:00
misiek
ae47e5268b Translated using Weblate (Polish)
Currently translated at 95.5% (193 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-01-27 16:17:10 +00:00
Jonas Schäfer
4b4844ecaa Clean up icon use throughout the thing 2021-01-27 17:16:45 +01:00
Jonas Schäfer
0ead8ce4b3 Improve navigation in circles section
Separate the people/edit buttons visually (although they lead to
the same page) to anticipate user intent.
2021-01-27 17:16:35 +01:00
Jonas Schäfer
d568a235eb Fix breaking the vertical rhythm in the theme demo 2021-01-27 17:16:30 +01:00
Jonas Schäfer
c3ce7d9f3a Left-align table headers
This lets the layout look less ragged.
2021-01-27 17:16:30 +01:00
Jonas Schäfer
4d0ba8ef9a Tweak form layout
- Use boldface for labels
- Use the equivalent of h4 for the heading
- Indent checkbox/radio choices
2021-01-27 17:16:30 +01:00
Jonas Schäfer
2f368e0a34 Change "reusability" to "type" for invites
- This makes the choice much clearer
- Allows for less generic terms in the table
- Future extensibility \o/
2021-01-27 17:16:30 +01:00
Weblate
ad0041ba84 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-27 14:35:46 +00:00
uira
561d576934 Translated using Weblate (Indonesian)
Currently translated at 100.0% (202 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-01-27 14:35:46 +00:00
arco
a642daf77a Translated using Weblate (French)
Currently translated at 100.0% (202 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-27 14:35:46 +00:00
misiek
7018f03c34 Translated using Weblate (Polish)
Currently translated at 86.1% (174 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-01-27 14:35:46 +00:00
Jonas Schäfer
7da56c81fc Make default circle un-editable in the UI 2021-01-27 15:34:30 +01:00
Jonas Schäfer
2b7930a5b7 Update strings after removal of things 2021-01-27 15:34:09 +01:00
Jonas Schäfer
d556034349 Drop phone number from the UI
It cannot be set with any tools currently, so we don’t need to
waste space on it.
2021-01-27 15:03:36 +01:00
Matthew Wild
d8341455a2 docker: Don't fail build if translations fail (temporary?) 2021-01-27 13:25:21 +00:00
franck
f5c7b9f0e3 Translated using Weblate (French)
Currently translated at 75.7% (153 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 21:57:17 +00:00
arco
a86d033f20 Translated using Weblate (French)
Currently translated at 75.7% (153 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 21:57:16 +00:00
pep
1fd51b00ed Translated using Weblate (French)
Currently translated at 75.7% (153 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 21:57:16 +00:00
franck
95ec9adfcd Translated using Weblate (French)
Currently translated at 75.7% (153 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 21:55:09 +00:00
arco
3446f57478 Translated using Weblate (French)
Currently translated at 75.7% (153 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 21:55:08 +00:00
Weblate
dd607af1ae Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-26 19:51:34 +00:00
pep
ea4bb8d98e Translated using Weblate (French)
Currently translated at 74.2% (150 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 19:51:34 +00:00
arco
61687e3158 Translated using Weblate (French)
Currently translated at 74.2% (150 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 19:51:33 +00:00
franck
ca977ffec3 Translated using Weblate (French)
Currently translated at 74.2% (150 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 19:51:33 +00:00
misiek
f539493bf5 Translated using Weblate (Polish)
Currently translated at 86.1% (174 of 202 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-01-26 19:51:33 +00:00
Jonas Schäfer
6ffce26b08 Straighten up terminology
code -> link, invite -> invitation
2021-01-26 20:51:21 +01:00
Jonas Schäfer
a9f9f9d74a s/user name/username/
Also fix tense of error message.
2021-01-26 20:51:21 +01:00
Weblate
b338b0a08f Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-26 15:28:31 +00:00
Jonas Schäfer
1a4f16eaef Fix typo 2021-01-26 16:28:23 +01:00
Weblate
2521aa98af Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-26 15:14:50 +00:00
Jonas Schäfer
a6d20a0a73 Change generic Apply labels to something more semantic
In context of #33.
2021-01-26 16:12:23 +01:00
Jonas Schäfer
df75fbaa1b Add support for circles with MUCs 2021-01-26 16:12:07 +01:00
Jonas Schäfer
5f1a45082e Add explanatory texts to circle related views 2021-01-26 16:11:56 +01:00
Weblate
9e2b6a4115 Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/
2021-01-26 14:06:40 +00:00
pep
91febde2a3 Translated using Weblate (Japanese)
Currently translated at 31.5% (47 of 149 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/ja/
2021-01-26 14:06:38 +00:00
Jonas Schäfer
b548dc011d Translated using Weblate (German)
Currently translated at 100.0% (149 of 149 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/de/
2021-01-26 14:06:38 +00:00
pep
88b8f675c1 Translated using Weblate (French)
Currently translated at 100.0% (149 of 149 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 14:06:38 +00:00
franck
693b1ac23f Translated using Weblate (French)
Currently translated at 100.0% (149 of 149 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/fr/
2021-01-26 14:06:38 +00:00
uira
7ec12a5958 Translated using Weblate (Indonesian)
Currently translated at 100.0% (149 of 149 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/id/
2021-01-26 14:06:38 +00:00
misiek
3a15406771 Translated using Weblate (Polish)
Currently translated at 100.0% (149 of 149 strings)

Translation: Snikket/Web Portal
Translate-URL: https://i18n.sotecware.net/projects/snikket/web-portal/pl/
2021-01-26 14:06:38 +00:00
Jonas Schäfer
cc27256b14 Merge branch 'feature/onboarding' 2021-01-26 15:06:24 +01:00
86 changed files with 18845 additions and 2965 deletions

View File

@@ -1,23 +0,0 @@
name: Docker image build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: >-
docker build . \
--build-arg=BUILD_SERIES=dev \
--build-arg=BUILD_ID="$(echo "$GITHUB_SHA" | head -c 12)" \
--tag snikket/snikket-web-portal:dev
- name: Log into registry
run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u snikket --password-stdin
- name: Push the Docker image
run: docker push snikket/snikket-web-portal:dev

View File

@@ -1,31 +0,0 @@
---
name: Docker release image build
"on":
push:
tags:
- release/*.*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: >-
echo "Building ref $GITHUB_REF...";
RELEASE_TAG="${GITHUB_REF#refs/tags/release/}";
RELEASE_SERIES="${RELEASE_TAG%.*}";
RELEASE_VER="${RELEASE_TAG#$RELEASE_SERIES.}";
docker build . \
--build-arg=BUILD_SERIES="$RELEASE_SERIES" \
--build-arg=BUILD_ID="$RELEASE_VER" \
--tag snikket/snikket-web-portal:"$RELEASE_SERIES"
- name: Log into registry
run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u snikket --password-stdin
- name: Push the Docker image
run: >-
RELEASE_TAG="${GITHUB_REF#refs/tags/release/}";
RELEASE_SERIES="${RELEASE_TAG%.*}";
docker push snikket/snikket-web-portal:"$RELEASE_SERIES"

View File

@@ -27,6 +27,7 @@ jobs:
set -euo pipefail
pip install mypy
pip install -r requirements.txt
pip install -r build-requirements.txt
- name: Typecheck
run: |
python -m mypy --config mypy.ini -p snikket_web
@@ -48,3 +49,12 @@ jobs:
- name: Linting
run: |
python -m flake8 snikket_web
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: >-
docker build .

View File

@@ -1,46 +1,31 @@
FROM debian:buster
FROM debian:buster-slim
ARG BUILD_SERIES=dev
ARG BUILD_ID=0
ENV DEBIAN_FRONTEND noninteractive
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
COPY Makefile /opt/snikket-web-portal/Makefile
COPY snikket_web/ /opt/snikket-web-portal/snikket_web
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
# This Dockerfile attempts to strike a balance between image size and time it
# takes to do an incremental build on changes.
# Improvements welcome.
WORKDIR /opt/snikket-web-portal
RUN set -eu; \
export DEBIAN_FRONTEND=noninteractive ; \
apt-get update ; \
apt-get install -y --no-install-recommends \
python3 python3-pip python3-setuptools python3-wheel \
libpython3-dev \
make build-essential \
netcat \
; \
apt-get clean ; rm -rf /var/lib/apt/lists
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
WORKDIR /opt/snikket-web-portal
RUN set -eu; \
pip3 install -r requirements.txt; \
pip3 install -r build-requirements.txt; \
rm -rf /root/.cache;
COPY Makefile /opt/snikket-web-portal/Makefile
COPY snikket_web/ /opt/snikket-web-portal/snikket_web
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
# NOTE: abusing true(1) as a terrible way to disable a specific command. If
# one merged all the RUN commands into one, one would want to run the
# uninstall/remove commands there, but with the split up RUN commands it is
# rather pointless.
RUN set -eu; \
make; \
true pip3 uninstall -yr build-requirements.txt; \
true apt-get remove -y build-essential make libpython3-dev; \
true apt-get autoremove -y; \
pip3 uninstall -yr build-requirements.txt; \
apt-get remove -y build-essential make libpython3-dev; \
apt-get autoremove -y; \
pip3 install hypercorn; \
rm -rf /root/.cache; \
apt-get clean ; rm -rf /var/lib/apt/lists
@@ -48,5 +33,11 @@ RUN set -eu; \
COPY docker/env.py /etc/snikket-web-portal/env.py
ENV SNIKKET_WEB_PYENV=/etc/snikket-web-portal/env.py
ENV SNIKKET_WEB_PROSODY_ENDPOINT=http://127.0.0.1:5280/
HEALTHCHECK CMD nc -zv ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE:-127.0.0.1} ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT:-5765}
RUN echo "$BUILD_SERIES $BUILD_ID" > /opt/snikket-web-portal/.app_version
ADD docker/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]

View File

@@ -31,7 +31,7 @@ force_update_translations: extract_translations
pybabel update -i $(pot_file) -d $(translation_basepath)
compile_translations:
pybabel compile -d $(translation_basepath)
-pybabel compile -d $(translation_basepath)
.PHONY: build_css clean update_translations compile_translations extract_translations force_update_translations

View File

@@ -1,3 +1,4 @@
pyscss~=1.3
mypy
python-dotenv~=0.15
types-toml

View File

@@ -1,2 +1,8 @@
#!/bin/sh
exec hypercorn -b "0.0.0.0:8000" 'snikket_web:create_app()'
export SNIKKET_WEB_DOMAIN="$SNIKKET_DOMAIN"
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE-127.0.0.1}"
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT-5765}"
exec hypercorn -b "${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE}:${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT}" 'snikket_web:create_app()'

View File

@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
@@ -9,426 +7,363 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="334mm"
height="154mm"
viewBox="0 0 334 154"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="colours.svg"
inkscape:export-filename="/home/horazont/tmp/colours.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
inkscape:cx="642.84838"
inkscape:cy="251.88403"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1401"
inkscape:window-x="0"
inkscape:window-y="39"
inkscape:window-maximized="0"
fit-margin-top="5"
fit-margin-left="5"
fit-margin-right="5"
fit-margin-bottom="5" />
id="svg1424"
sodipodi:docname="out.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<metadata
id="metadata5">
id="metadata1430">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(65,-148)">
<rect
style="opacity:1;fill:#418fc7;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4759"
width="24"
height="24"
x="120"
y="273" />
<rect
y="273"
x="-2.7815501e-08"
height="24"
width="24"
id="rect9362"
style="opacity:1;fill:#062943;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="273"
x="240"
height="24"
width="24"
id="rect9364"
style="opacity:1;fill:#e4f3fd;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="273"
x="60"
height="24"
width="24"
id="rect9366"
style="opacity:1;fill:#0e4c76;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#9dccf0;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9368"
width="24"
height="24"
x="180"
y="273" />
<rect
style="opacity:1;fill:#0f3d62;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9370"
width="24"
height="24"
x="30"
y="273" />
<rect
y="273"
x="90"
height="24"
width="24"
id="rect9372"
style="opacity:1;fill:#226494;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="273"
x="150"
height="24"
width="24"
id="rect9376"
style="opacity:1;fill:#72b2e3;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="273"
x="210"
height="24"
width="24"
id="rect9378"
style="opacity:1;fill:#b5d8f3;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="243"
x="120"
height="24"
width="24"
id="rect9380"
style="opacity:1;fill:#c95e40;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#340e03;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9382"
width="24"
height="24"
x="0"
y="243" />
<rect
style="opacity:1;fill:#fef1ed;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9384"
width="24"
height="24"
x="240"
y="243" />
<rect
style="opacity:1;fill:#e2b00c;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9436"
width="24"
height="24"
x="120"
y="213" />
<rect
style="opacity:1;fill:#f2ac99;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9454"
width="24"
height="24"
x="180"
y="243" />
<rect
y="243"
x="210"
height="24"
width="24"
id="rect9456"
style="opacity:1;fill:#fbc2b3;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="243"
x="150"
height="24"
width="24"
id="rect9458"
style="opacity:1;fill:#ed947c;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#883017;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9460"
width="24"
height="24"
x="60"
y="243" />
<rect
y="243"
x="30"
height="24"
width="24"
id="rect9462"
style="opacity:1;fill:#681f0b;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="243"
x="90"
height="24"
width="24"
id="rect9464"
style="opacity:1;fill:#a33d21;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="183"
x="120"
height="24"
width="24"
id="rect9466"
style="opacity:1;fill:#55c644;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="213"
x="240"
height="24"
width="24"
id="rect9484"
style="opacity:1;fill:#fffcf0;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="213"
x="0"
height="24"
width="24"
id="rect9486"
style="opacity:1;fill:#302100;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="213"
x="60"
height="24"
width="24"
id="rect9488"
style="opacity:1;fill:#886600;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#563600;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9490"
width="24"
height="24"
x="30"
y="213" />
<rect
y="213"
x="90"
height="24"
width="24"
id="rect9494"
style="opacity:1;fill:#b98601;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="213"
x="180"
height="24"
width="24"
id="rect9496"
style="opacity:1;fill:#feed93;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="213"
x="150"
height="24"
width="24"
id="rect9500"
style="opacity:1;fill:#fde58a;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#fff7c2;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9502"
width="24"
height="24"
x="210"
y="213" />
<rect
style="opacity:1;fill:#8f8983;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9504"
width="24"
height="24"
x="120"
y="153" />
<rect
y="153"
x="0"
height="24"
width="24"
id="rect9506"
style="opacity:1;fill:#1f1b17;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="153"
x="240"
height="24"
width="24"
id="rect9508"
style="opacity:1;fill:#f6f5f4;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="153"
x="180"
height="24"
width="24"
id="rect9510"
style="opacity:1;fill:#cac3bd;fill-opacity:0.98872178;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#e3e1df;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9512"
width="24"
height="24"
x="210"
y="153" />
<rect
style="opacity:1;fill:#4e4a46;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9514"
width="24"
height="24"
x="60"
y="153" />
<rect
y="153"
x="30"
height="24"
width="24"
id="rect9516"
style="opacity:1;fill:#3d3833;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#706965;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9518"
width="24"
height="24"
x="90"
y="153" />
<rect
y="153"
x="150"
height="24"
width="24"
id="rect9520"
style="opacity:1;fill:#b1aca6;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#ecfbe6;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9522"
width="24"
height="24"
x="240"
y="183" />
<rect
style="opacity:1;fill:#052f03;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9524"
width="24"
height="24"
x="0"
y="183" />
<rect
style="opacity:1;fill:#197713;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9526"
width="24"
height="24"
x="60"
y="183" />
<rect
y="183"
x="30"
height="24"
width="24"
id="rect9528"
style="opacity:1;fill:#0c4608;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="183"
x="90"
height="24"
width="24"
id="rect9530"
style="opacity:1;fill:#218a1b;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:#abed9c;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9532"
width="24"
height="24"
x="180"
y="183" />
<rect
style="opacity:1;fill:#81e06e;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect9536"
width="24"
height="24"
x="150"
y="183" />
<rect
y="183"
x="210"
height="24"
width="24"
id="rect9538"
style="opacity:1;fill:#cef6c5;fill-opacity:1;stroke:none;stroke-width:0.32483485;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12.69999981px;line-height:1em;font-family:'Liberation Sans';-inkscape-font-specification:'Liberation Sans';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="-3.4391122"
y="170.58151"
id="text9546"><tspan
sodipodi:role="line"
id="tspan9544"
x="-3.4391129"
y="170.58151"
style="stroke-width:0.26458332px">Grayscale</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12.69999981px;line-height:1em;font-family:'Liberation Sans';-inkscape-font-specification:'Liberation Sans';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="-12.554834"
y="200.5157"
id="text9550"><tspan
sodipodi:role="line"
id="tspan9548"
x="-12.554835"
y="200.5157"
style="stroke-width:0.26458332px">Success</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12.69999981px;line-height:1em;font-family:'Liberation Sans';-inkscape-font-specification:'Liberation Sans';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="-21.180664"
y="230.71715"
id="text9554"><tspan
sodipodi:role="line"
id="tspan9552"
x="-21.180664"
y="230.71715"
style="stroke-width:0.26458332px">Accent</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12.69999981px;line-height:1em;font-family:'Liberation Sans';-inkscape-font-specification:'Liberation Sans';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="-33.880665"
y="261.45316"
id="text9558"><tspan
sodipodi:role="line"
id="tspan9556"
x="-33.880665"
y="261.45316"
style="stroke-width:0.26458332px">Alert</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12.69999981px;line-height:1em;font-family:'Liberation Sans';-inkscape-font-specification:'Liberation Sans';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="-17.236719"
y="290.85278"
id="text9562"><tspan
sodipodi:role="line"
id="tspan9560"
x="-17.236719"
y="290.85278"
style="stroke-width:0.26458332px">Primary</tspan></text>
</g>
<defs
id="defs1428" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1401"
id="namedview1426"
showgrid="false"
inkscape:zoom="3.9666667"
inkscape:cx="123.61033"
inkscape:cy="67.707413"
inkscape:window-x="0"
inkscape:window-y="39"
inkscape:window-maximized="0"
inkscape:current-layer="svg1424" />
<rect
style="fill: #000000;"
width="310"
height="240"
x="-25"
y="-25"
id="rect1332" />
<rect
style="fill: #1f1b17;"
width="20"
height="20"
x="0"
y="0"
id="rect1334" />
<rect
style="fill: #3d3833;"
width="20"
height="20"
x="30"
y="0"
id="rect1336" />
<rect
style="fill: #4e4a46;"
width="20"
height="20"
x="60"
y="0"
id="rect1338" />
<rect
style="fill: #706965;"
width="20"
height="20"
x="90"
y="0"
id="rect1340" />
<rect
style="fill: #8f8983;"
width="20"
height="20"
x="120"
y="0"
id="rect1342" />
<rect
style="fill: #b1aca6;"
width="20"
height="20"
x="150"
y="0"
id="rect1344" />
<rect
style="fill: #cac3bd;"
width="20"
height="20"
x="180"
y="0"
id="rect1346" />
<rect
style="fill: #e3e1df;"
width="20"
height="20"
x="210"
y="0"
id="rect1348" />
<rect
style="fill: #f6f5f4;"
width="20"
height="20"
x="240"
y="0"
id="rect1350" />
<rect
style="fill:#062243;fill-opacity:1"
width="20"
height="20"
x="0"
y="40"
id="rect1352" />
<rect
style="fill:#0f3462;fill-opacity:1"
width="20"
height="20"
x="30"
y="40"
id="rect1354" />
<rect
style="fill:#0e4276;fill-opacity:1"
width="20"
height="20"
x="60"
y="40"
id="rect1356" />
<rect
style="fill:#225994;fill-opacity:1"
width="20"
height="20"
x="90"
y="40"
id="rect1358" />
<rect
style="fill:#4182c7;fill-opacity:1"
width="20"
height="20"
x="120"
y="40"
id="rect1360" />
<rect
style="fill:#72a7e3;fill-opacity:1"
width="20"
height="20"
x="150"
y="40"
id="rect1362" />
<rect
style="fill:#9dc4f0;fill-opacity:1"
width="20"
height="20"
x="180"
y="40"
id="rect1364" />
<rect
style="fill:#b5d2f3;fill-opacity:1"
width="20"
height="20"
x="210"
y="40"
id="rect1366" />
<rect
style="fill:#e4f0fd;fill-opacity:1"
width="20"
height="20"
x="240"
y="40"
id="rect1368" />
<rect
style="fill: #340e03;"
width="20"
height="20"
x="0"
y="80"
id="rect1370" />
<rect
style="fill: #681f0b;"
width="20"
height="20"
x="30"
y="80"
id="rect1372" />
<rect
style="fill: #883017;"
width="20"
height="20"
x="60"
y="80"
id="rect1374" />
<rect
style="fill: #a33d21;"
width="20"
height="20"
x="90"
y="80"
id="rect1376" />
<rect
style="fill: #c95e40;"
width="20"
height="20"
x="120"
y="80"
id="rect1378" />
<rect
style="fill: #ed947c;"
width="20"
height="20"
x="150"
y="80"
id="rect1380" />
<rect
style="fill: #f2ac99;"
width="20"
height="20"
x="180"
y="80"
id="rect1382" />
<rect
style="fill: #fbc2b3;"
width="20"
height="20"
x="210"
y="80"
id="rect1384" />
<rect
style="fill: #fef1ed;"
width="20"
height="20"
x="240"
y="80"
id="rect1386" />
<rect
style="fill: #302100;"
width="20"
height="20"
x="0"
y="120"
id="rect1388" />
<rect
style="fill: #563600;"
width="20"
height="20"
x="30"
y="120"
id="rect1390" />
<rect
style="fill: #795b00;"
width="20"
height="20"
x="60"
y="120"
id="rect1392" />
<rect
style="fill: #a07501;"
width="20"
height="20"
x="90"
y="120"
id="rect1394" />
<rect
style="fill: #c79b0e;"
width="20"
height="20"
x="120"
y="120"
id="rect1396" />
<rect
style="fill: #f4ce3f;"
width="20"
height="20"
x="150"
y="120"
id="rect1398" />
<rect
style="fill:#fee577;fill-opacity:1"
width="20"
height="20"
x="180"
y="120"
id="rect1400" />
<rect
style="fill:#fef1c1;fill-opacity:1"
width="20"
height="20"
x="210"
y="120"
id="rect1402" />
<rect
style="fill:#fff8e8;fill-opacity:1"
width="20"
height="20"
x="240"
y="120"
id="rect1404" />
<rect
style="fill:#172f03;fill-opacity:1"
width="20"
height="20"
x="0"
y="160"
id="rect1406" />
<rect
style="fill:#244608;fill-opacity:1"
width="20"
height="20"
x="30"
y="160"
id="rect1408" />
<rect
style="fill:#407713;fill-opacity:1"
width="20"
height="20"
x="60"
y="160"
id="rect1410" />
<rect
style="fill:#548f19;fill-opacity:1"
width="20"
height="20"
x="90"
y="160"
id="rect1412" />
<rect
style="fill:#7fc644;fill-opacity:1"
width="20"
height="20"
x="120"
y="160"
id="rect1414" />
<rect
style="fill:#a1e06e;fill-opacity:1"
width="20"
height="20"
x="150"
y="160"
id="rect1416" />
<rect
style="fill:#c0ed9c;fill-opacity:1"
width="20"
height="20"
x="180"
y="160"
id="rect1418" />
<rect
style="fill:#dbf6c5;fill-opacity:1"
width="20"
height="20"
x="210"
y="160"
id="rect1420" />
<rect
style="fill:#effbe6;fill-opacity:1"
width="20"
height="20"
x="240"
y="160"
id="rect1422" />
</svg>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 421 KiB

View File

@@ -1,8 +1,9 @@
aiohttp~=3.6
quart~=0.11
quart~=0.11,<0.15
flask-wtf~=0.14
hsluv~=0.0.2
flask-babel~=1.0
email-validator~=1.1
environ-config~=20.0
wtforms~=2.3
typing-extensions

View File

@@ -15,12 +15,13 @@ from quart import (
render_template,
current_app,
redirect,
jsonify,
)
import environ
from . import colour, infra
from ._version import version, version_info # noqa:F401
from ._version import version # noqa:F401
async def proc() -> typing.Dict[str, typing.Any]:
@@ -47,6 +48,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
"text_to_css": colour.text_to_css,
"lang": infra.selected_locale(),
"user_info": user_info,
"is_in_debug_mode": current_app.debug,
}
@@ -142,7 +144,25 @@ class AppConfig:
domain = environ.var()
site_name = environ.var("")
avatar_cache_ttl = environ.var(1800, converter=int)
languages = environ.var(["de", "en"], converter=autosplit)
languages = environ.var([
"da",
"de",
"en",
"fr",
"id",
"it",
"pl",
"sv",
], converter=autosplit)
apple_store_url = environ.var(
"https://apps.apple.com/us/app/snikket/id1545164189",
)
# Default limit of 1 MiB is what was discovered to be the effective limit
# in #67, hence we set that here for now.
# Future versions may change this default, and the standard deployment
# tools may also very well override it.
max_avatar_size = environ.var(1024*1024, converter=int)
show_metrics = environ.bool_var(True)
_UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1)))
@@ -155,7 +175,7 @@ def create_app() -> quart.Quart:
pass
else:
import runpy
init_vars = runpy.run_path(env_init) # type:ignore
init_vars = runpy.run_path(env_init)
for name, value in init_vars.items():
if not name:
continue
@@ -172,6 +192,9 @@ def create_app() -> quart.Quart:
app.config["SNIKKET_DOMAIN"] = config.domain
app.config["SITE_NAME"] = config.site_name or config.domain
app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl
app.config["APPLE_STORE_URL"] = config.apple_store_url
app.config["MAX_AVATAR_SIZE"] = config.max_avatar_size
app.config["SHOW_METRICS"] = config.show_metrics
app.context_processor(proc)
app.register_error_handler(
@@ -194,6 +217,44 @@ def create_app() -> quart.Quart:
return redirect(url_for('main.login'))
@app.route("/site.webmanifest")
def site_manifest() -> quart.Response:
# this is needed for icons
return jsonify(
{
"name": "Snikket",
"short_name": "Snikket",
"icons": [
{
"src": url_for(
"static",
filename="img/android-chrome-192x192.png",
),
"sizes": "192x192",
"type": "image/png"
},
{
"src": url_for(
"static",
filename="img/android-chrome-256x256.png",
),
"sizes": "256x256",
"type": "image/png"
},
{
"src": url_for(
"static",
filename="img/android-chrome-512x512.png",
),
"sizes": "512x512",
"type": "image/png"
},
],
"theme_color": "#fbfdff",
"background_color": "#fbfdff",
}
)
logging_config = app.config.get("LOGGING_CONFIG")
if logging_config is not None:
if isinstance(logging_config, dict):

View File

@@ -1,5 +1,15 @@
version_info = (0, 1, 0, "a0")
version = (
".".join(map(str, version_info[:3])) +
(f"-{version_info[3]}" if version_info[3] else "")
)
import os
import subprocess
version = "(unknown)"
if os.path.exists(".app_version"):
with open(".app_version") as f:
version = f.read().strip()
elif os.path.exists(".git"):
try:
version = subprocess.check_output([
"git", "describe", "--always"
]).strip().decode("utf8")
except OSError:
version = "dev (unknown)"

View File

@@ -1,5 +1,6 @@
import asyncio
import json
import resource
import time
import typing
from datetime import datetime
@@ -18,13 +19,14 @@ from quart import (
url_for,
request,
abort,
flash,
current_app,
)
import flask_wtf
from flask_babel import lazy_gettext as _l
from flask_babel import lazy_gettext as _l, _
from . import prosodyclient
from .infra import client, circle_name
from . import prosodyclient, _version
from .infra import client, circle_name, BaseForm
bp = Blueprint("admin", __name__, url_prefix="/admin")
@@ -32,11 +34,14 @@ bp = Blueprint("admin", __name__, url_prefix="/admin")
@bp.route("/")
@client.require_admin_session()
async def index() -> str:
return await render_template("admin_home.html")
show_metrics = current_app.config["SHOW_METRICS"]
return await render_template(
"admin_home.html",
show_metrics=show_metrics,
)
class PasswordResetLinkPost(flask_wtf.FlaskForm): # type: ignore
action_create = wtforms.StringField()
class PasswordResetLinkPost(BaseForm):
action_revoke = wtforms.StringField()
@@ -47,15 +52,94 @@ async def users() -> str:
await client.list_users(),
key=lambda x: x.localpart
)
invite_form = InvitePost()
await invite_form.init_choices()
reset_form = PasswordResetLinkPost()
return await render_template(
"admin_users.html",
users=users,
reset_form=reset_form,
invite_form=invite_form,
)
class DeleteUserForm(flask_wtf.FlaskForm): # type:ignore
class EditUserForm(BaseForm):
localpart = wtforms.StringField(
_l("Login name"),
)
display_name = wtforms.StringField(
_l("Display name"),
)
role = wtforms.RadioField(
_l("Access Level"),
choices=[
("prosody:restricted", _("Limited")),
("prosody:normal", _l("Normal user")),
("prosody:admin", _l("Administrator")),
],
)
action_save = wtforms.SubmitField(
_l("Update user"),
)
action_create_reset = wtforms.SubmitField(
_l("Create password reset link"),
)
@bp.route("/user/<localpart>/", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_user(localpart: str) -> typing.Union[quart.Response, str]:
target_user_info = await client.get_user_by_localpart(localpart)
form = EditUserForm()
if form.validate_on_submit():
if form.action_create_reset.data:
target_user_info = await client.get_user_by_localpart(localpart)
reset_link = await client.create_password_reset_invite(
localpart=localpart,
ttl=86400,
)
await flash(
_("Password reset link created"),
"success",
)
return redirect(url_for(
".user_password_reset_link",
id_=reset_link.id_,
))
await client.update_user(
localpart,
display_name=form.display_name.data,
roles=[form.role.data],
)
await flash(
_("User information updated."),
"success",
)
return redirect(url_for(".edit_user", localpart=localpart))
elif request.method == "GET":
form.localpart.data = target_user_info.localpart
form.display_name.data = target_user_info.display_name
if target_user_info.roles:
form.role.data = target_user_info.roles[0]
else:
form.role.data = "prosody:normal"
return await render_template(
"admin_edit_user.html",
target_user=target_user_info,
form=form,
)
class DeleteUserForm(BaseForm):
action_delete = wtforms.SubmitField(
_l("Delete user permanently")
)
@@ -69,6 +153,10 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
if form.validate_on_submit():
if form.action_delete.data:
await client.delete_user_by_localpart(localpart)
await flash(
_("User deleted"),
"success",
)
return redirect(url_for(".users"))
return await render_template(
@@ -94,37 +182,47 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
)
@bp.route("/users/password-reset/-", methods=["POST"])
@bp.route("/users/password-reset/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def create_password_reset_link() -> typing.Union[str, quart.Response]:
form = PasswordResetLinkPost()
if not form.validate_on_submit():
abort(400)
if form.action_create.data:
localpart = form.action_create.data
target_user_info = await client.get_user_by_localpart(localpart)
reset_link = await client.create_password_reset_invite(
localpart=localpart,
ttl=86400,
async def user_password_reset_link(
id_: str,
) -> typing.Union[str, quart.Response]:
invite_info = await client.get_invite_by_id(
id_,
)
if invite_info.jid is None:
await flash(
_("Password reset link not found"),
"alert",
)
elif form.action_revoke.data:
await client.delete_invite(form.action_revoke.data)
return redirect(url_for(".users"))
localpart = prosodyclient.split_jid(invite_info.jid)[0]
form = PasswordResetLinkPost()
if form.validate_on_submit():
if form.action_revoke.data:
await client.delete_invite(id_)
await flash(
_("Password reset link deleted"),
"success",
)
return redirect(url_for(".edit_user", localpart=localpart))
abort(400)
return await render_template(
"admin_reset_user_password.html",
target_user=target_user_info,
reset_link=reset_link,
localpart=localpart,
reset_link=invite_info,
form=form,
)
class InvitesListForm(flask_wtf.FlaskForm): # type:ignore
class InvitesListForm(BaseForm):
action_revoke = wtforms.StringField()
class InvitePost(flask_wtf.FlaskForm): # type:ignore
class InvitePost(BaseForm):
circles = wtforms.SelectMultipleField(
_l("Invite to circle"),
# NOTE: This is for when/if we ever support multi-group invites.
@@ -148,8 +246,13 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
default=7*86400,
)
reusable = wtforms.BooleanField(
_l("Invite a group of people"),
type_ = wtforms.RadioField(
_l("Invitation type"),
choices=[
("account", _l("Individual")),
("group", _l("Group")),
],
default="account",
)
action_create_invite = wtforms.SubmitField(
@@ -213,7 +316,7 @@ async def invitations() -> typing.Union[str, quart.Response]:
)
class InviteForm(flask_wtf.FlaskForm): # type:ignore
class InviteForm(BaseForm):
action_revoke = wtforms.SubmitField(
_l("Revoke")
)
@@ -228,7 +331,7 @@ async def create_invite() -> typing.Union[str, quart.Response]:
(c.id_, c.name) for c in circles
]
if form.validate_on_submit():
if form.reusable.data:
if form.type_.data == "group":
invite = await client.create_group_invite(
group_ids=form.circles.data,
ttl=form.lifetime.data,
@@ -238,6 +341,10 @@ async def create_invite() -> typing.Union[str, quart.Response]:
group_ids=form.circles.data,
ttl=form.lifetime.data,
)
await flash(
_("Invitation created"),
"success",
)
return redirect(url_for(".edit_invite", id_=invite.id_))
return await render_template("admin_create_invite.html",
invite_form=form)
@@ -246,7 +353,15 @@ async def create_invite() -> typing.Union[str, quart.Response]:
@bp.route("/invitation/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
invite_info = await client.get_invite_by_id(id_)
try:
invite_info = await client.get_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
await flash(
_("No such invitation exists"),
"alert",
)
return redirect(url_for(".invitations"))
circles = await client.list_groups()
circle_map = {
circle.id_: circle
@@ -257,6 +372,10 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
if form.validate_on_submit():
if form.action_revoke.data:
await client.delete_invite(id_)
await flash(
_("Invitation revoked"),
"success",
)
return redirect(url_for(".invitations"))
return redirect(url_for(".edit_invite", id_=id_))
@@ -269,7 +388,7 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
)
class CirclePost(flask_wtf.FlaskForm): # type:ignore
class CirclePost(BaseForm):
name = wtforms.StringField(
_l("Name"),
validators=[wtforms.validators.InputRequired()],
@@ -305,6 +424,10 @@ async def create_circle() -> typing.Union[str, quart.Response]:
circle = await client.create_group(
name=create_form.name.data,
)
await flash(
_("Circle created"),
"success",
)
return redirect(url_for(".edit_circle", id_=circle.id_))
return await render_template(
@@ -313,7 +436,7 @@ async def create_circle() -> typing.Union[str, quart.Response]:
)
class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
class EditCircleForm(BaseForm):
name = wtforms.StringField(
_l("Name"),
validators=[wtforms.validators.InputRequired()],
@@ -325,7 +448,7 @@ class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
)
action_save = wtforms.SubmitField(
_l("Apply")
_l("Update circle")
)
action_delete = wtforms.SubmitField(
@@ -350,27 +473,27 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
await flash(
_("No such circle exists"),
"alert",
)
return redirect(url_for(".circles"))
raise
circle_members = await asyncio.gather(*(
client.get_user_by_localpart(
localpart,
session=session,
)
users = {
user.localpart: user
for user in await client.list_users()
}
circle_members = [
(localpart, users.get(localpart))
for localpart in sorted(circle.members)
))
users = await client.list_users()
]
form = EditCircleForm()
form.user_to_add.choices = sorted(
(
(u.localpart, u.localpart)
for u in users
if u.localpart not in circle.members
),
key=lambda x: x[1]
(localpart, localpart)
for localpart in users.keys()
if localpart not in circle.members
)
valid_users = [x[0] for x in form.user_to_add.choices]
@@ -387,21 +510,36 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
id_,
new_name=form.name.data,
)
await flash(
_("Circle data updated"),
"success",
)
elif form.action_delete.data:
await client.delete_group(id_)
await flash(
_("Circle deleted"),
"success",
)
return redirect(url_for(".circles"))
elif form.action_add_user.data:
if form.user_to_add.data in valid_users:
print("is valid")
await client.add_group_member(
id_,
form.user_to_add.data,
)
await flash(
_("User added to circle"),
"success",
)
elif form.action_remove_user.data:
await client.remove_group_member(
id_,
form.action_remove_user.data,
)
await flash(
_("User removed from circle"),
"success",
)
return redirect(url_for(".edit_circle", id_=id_))
else:
@@ -414,3 +552,148 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
circle_members=circle_members,
invite_form=invite_form,
)
_CPU_EPOCH = time.process_time()
_MONOTONIC_EPOCH = time.monotonic()
def get_system_stats() -> typing.MutableMapping[
str,
typing.Optional[typing.Union[int, float]]]:
pagesize = resource.getpagesize()
my_rss: typing.Optional[int] = None
try:
with open("/proc/self/statm") as f:
stats = f.read().split()
my_rss = int(stats[1]) * pagesize
except (ValueError, IndexError, TypeError, OSError):
pass
my_cpu = (
(time.process_time() - _CPU_EPOCH) /
(time.monotonic() - _MONOTONIC_EPOCH)
)
mem_total, mem_available = None, None
load5: typing.Optional[float] = None
try:
with open("/proc/loadavg") as f:
stats = f.read().split()
load5 = float(stats[1])
except (ValueError, IndexError, TypeError, OSError):
pass
try:
with open("/proc/meminfo") as f:
for line in f:
if line.startswith("MemTotal"):
mem_total = int(line.split()[1]) * 1024
elif line.startswith("MemAvailable"):
mem_available = int(line.split()[1]) * 1024
if mem_total is not None and mem_available is not None:
break
except (ValueError, TypeError, IndexError, OSError):
pass
return {
"portal_rss": my_rss,
"portal_cpu": my_cpu,
"load5": load5,
"mem_total": mem_total,
"mem_available": mem_available,
}
class AnnouncementForm(BaseForm):
text = wtforms.StringField(
_("Message contents"),
widget=wtforms.widgets.TextArea(),
validators=[wtforms.validators.DataRequired()],
)
online_only = wtforms.BooleanField(
_("Only send to online users"),
)
action_post_all = wtforms.SubmitField(
_("Post to all users"),
)
action_send_preview = wtforms.SubmitField(
_("Send preview to yourself"),
)
@bp.route("/system/", methods=["GET", "POST"])
@client.require_admin_session()
async def system() -> typing.Union[str, quart.Response]:
form = AnnouncementForm()
if form.validate_on_submit():
recipients = "self"
if form.action_post_all.data:
if form.online_only.data:
recipients = "online"
else:
recipients = "all"
await client.post_announcement(
form.text.data,
recipients=recipients,
)
await flash(
_("Announcement sent!"),
"success",
)
if recipients != "self":
# redirect only if not previewing
return redirect(url_for(".system"))
version = None
now = None
show_metrics = current_app.config["SHOW_METRICS"]
if show_metrics:
version = await client.get_server_version()
now = time.time()
try:
prosody_metrics = await client.get_system_metrics()
except quart.exceptions.NotFound:
# server does not offer the endpoint for whatever reason -- ignore
prosody_metrics = {}
metrics = get_system_stats()
try:
prosody_cpu_metrics = prosody_metrics["cpu"]
except KeyError:
pass
else:
metrics["prosody_cpu"] = (prosody_cpu_metrics["value"] /
(now - prosody_cpu_metrics["since"]))
try:
metrics["prosody_rss"] = prosody_metrics["memory"]
except KeyError:
pass
try:
metrics["prosody_devices"] = prosody_metrics["c2s"]
except KeyError:
pass
for k in list(metrics.keys()):
if metrics[k] is None:
# so that defaulting in jinja works
del metrics[k]
else:
metrics = {}
return await render_template(
"admin_system.html",
metrics=metrics,
version=_version.version,
prosody_version=version,
form=form,
show_metrics=show_metrics,
)

View File

@@ -1,5 +1,6 @@
import base64
import itertools
import math
import secrets
import typing
@@ -10,6 +11,7 @@ from quart import (
)
import flask_babel
import flask_wtf
from flask_babel import _
from . import prosodyclient
@@ -21,11 +23,20 @@ client.default_login_redirect = "main.login"
babel = flask_babel.Babel()
BYTE_UNIT_SCALE_MAP = [
"B",
"kiB",
"MiB",
"GiB",
"TiB",
]
@babel.localeselector # type:ignore
def selected_locale() -> str:
selected = request.accept_languages.best_match(
current_app.config['LANGUAGES']
)
) or current_app.config['LANGUAGES'][0]
return selected
@@ -41,12 +52,27 @@ def circle_name(c: typing.Any) -> str:
return c.name
def format_bytes(n: float) -> str:
scale = math.floor(math.log(n, 1024))
try:
unit = BYTE_UNIT_SCALE_MAP[scale]
factor = 1024**scale
except ValueError:
unit = "TiB"
factor = 1024**4
if factor > 1:
return "{:.1f}{}".format(n / factor, unit)
return "{}{}".format(n, unit)
def init_templating(app: quart.Quart) -> None:
app.template_filter("repr")(repr)
app.template_filter("format_datetime")(flask_babel.format_datetime)
app.template_filter("format_date")(flask_babel.format_date)
app.template_filter("format_time")(flask_babel.format_time)
app.template_filter("format_timedelta")(flask_babel.format_timedelta)
app.template_filter("format_percent")(flask_babel.format_percent)
app.template_filter("format_bytes")(format_bytes)
app.template_filter("flatten")(flatten)
app.template_filter("circle_name")(circle_name)
@@ -55,3 +81,14 @@ def generate_error_id() -> str:
return base64.b32encode(secrets.token_bytes(8)).decode(
"ascii"
).rstrip("=")
class BaseForm(flask_wtf.FlaskForm): # type:ignore
def __init__(self, *args: typing.Any, **kwargs: typing.Any):
meta = kwargs["meta"] = dict(kwargs.get("meta", {}))
if "locales" not in meta:
locale = flask_babel.get_locale()
if locale:
meta["locales"] = [str(locale)]
super().__init__(*args, **kwargs)

View File

@@ -7,6 +7,7 @@ import aiohttp
import quart.flask_patch
from quart import (
Blueprint,
current_app,
render_template,
redirect,
url_for,
@@ -15,10 +16,9 @@ from quart import (
import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l
from .infra import client, selected_locale
from .infra import client, selected_locale, BaseForm
bp = Blueprint("invite", __name__)
@@ -47,15 +47,30 @@ def context() -> typing.Mapping[str, typing.Any]:
@bp.route("/<id_>")
async def view(id_: str) -> str:
async def view_old(id_: str) -> quart.Response:
return redirect(url_for(".view", id_=id_))
@bp.route("/<id_>/")
async def view(id_: str) -> typing.Union[quart.Response,
typing.Tuple[str, int],
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")
return await render_template("invite_invalid.html"), 404
raise
if invite.reset_localpart is not None:
return await render_template(
"invite_reset_view.html",
invite=invite,
invite_id=id_,
account_jid="{}@{}".format(invite.reset_localpart, invite.domain)
)
play_store_url = (
"https://play.google.com/store/apps/details?" +
urllib.parse.urlencode(
@@ -68,20 +83,25 @@ async def view(id_: str) -> str:
),
)
)
apple_store_url = (
"https://apps.apple.com/us/app/tigase-messenger/id1153516838"
)
apple_store_url = current_app.config["APPLE_STORE_URL"]
return await render_template(
body = await render_template(
"invite_view.html",
invite=invite,
play_store_url=play_store_url,
apple_store_url=apple_store_url,
f_droid_url="market://details?id=org.snikket.android",
invite_id=id_,
)
return quart.Response(
body,
headers={
"Link": "<{}> rel=\"alternate\"".format(invite.xmpp_uri),
}
)
class RegisterForm(flask_wtf.FlaskForm): # type:ignore
class RegisterForm(BaseForm):
localpart = wtforms.StringField(
_l("Username"),
)
@@ -95,7 +115,7 @@ class RegisterForm(flask_wtf.FlaskForm): # type:ignore
validators=[wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"password",
_l("The passwords must match")
_l("The passwords must match.")
)]
)
@@ -106,7 +126,14 @@ class RegisterForm(flask_wtf.FlaskForm): # type:ignore
@bp.route("/<id_>/register", methods=["GET", "POST"])
async def register(id_: str) -> typing.Union[str, quart.Response]:
invite = await client.get_public_invite_by_id(id_)
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
return redirect(url_for(".view", id_=id_))
if invite.reset_localpart is not None:
return redirect(url_for(".reset", id_=id_))
form = RegisterForm()
if form.validate_on_submit():
@@ -120,15 +147,15 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
except aiohttp.ClientResponseError as exc:
if exc.status == 409:
form.localpart.errors.append(
_l("That user name is already taken")
_l("That username is already taken.")
)
elif exc.status == 403:
form.localpart.errors.append(
_l("Registration was declined for unknown reasons")
_l("Registration was declined for unknown reasons.")
)
elif exc.status == 400:
form.localpart.errors.append(
_l("The user name was not valid")
_l("The username is not valid.")
)
elif exc.status == 404:
return redirect(url_for(".view", id_=id_))
@@ -145,6 +172,66 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
)
class ResetForm(BaseForm):
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_reset = wtforms.SubmitField(
_l("Change password")
)
@bp.route("/<id_>/reset", methods=["GET", "POST"])
async def reset(id_: str) -> typing.Union[str, quart.Response]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
return redirect(url_for(".view", id_=id_))
if invite.reset_localpart is None:
return redirect(url_for(".register", id_=id_))
form = ResetForm()
if form.validate_on_submit():
# log the user in? show a guide? no idea.
try:
jid = await client.register_with_token(
username=invite.reset_localpart,
password=form.password.data,
token=id_,
)
except aiohttp.ClientResponseError as exc:
if exc.status == 403:
form.localpart.errors.append(
_l("Registration was declined for unknown reasons.")
)
elif exc.status == 404:
return redirect(url_for(".view", id_=id_))
else:
raise
else:
http_session[INVITE_SESSION_JID] = jid
return redirect(url_for(".reset_success"))
return await render_template(
"invite_reset.html",
invite=invite,
form=form,
)
@bp.route("/success", methods=["GET", "POST"])
async def success() -> str:
return await render_template(
@@ -153,6 +240,14 @@ async def success() -> str:
)
@bp.route("/success/reset", methods=["GET", "POST"])
async def reset_success() -> str:
return await render_template(
"invite_reset_success.html",
jid=http_session.get(INVITE_SESSION_JID, ""),
)
@bp.route("/-")
async def index() -> quart.Response:
return redirect(url_for("index"))

View File

@@ -15,23 +15,23 @@ from quart import (
render_template,
request,
Response,
flash,
)
import babel
import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l, _
from . import xmpputil, _version
from .infra import client
from .infra import client, BaseForm
bp = quart.Blueprint("main", __name__)
class LoginForm(flask_wtf.FlaskForm): # type:ignore
class LoginForm(BaseForm):
address = wtforms.TextField(
_l("Address"),
validators=[wtforms.validators.InputRequired()],
@@ -52,6 +52,9 @@ async def index() -> quart.Response:
return redirect(url_for("index"))
ERR_CREDENTIALS_INVALID = _l("Invalid username or password.")
@bp.route("/login", methods=["GET", "POST"])
async def login() -> typing.Union[str, quart.Response]:
if client.has_session and (await client.test_session()):
@@ -63,33 +66,48 @@ async def login() -> typing.Union[str, quart.Response]:
localpart, domain, resource = xmpputil.split_jid(jid)
if not localpart:
localpart, domain = domain, current_app.config["SNIKKET_DOMAIN"]
jid = "{}@{}".format(localpart, domain)
password = form.password.data
try:
await client.login(jid, password)
except quart.exceptions.Unauthorized:
form.password.errors.append(
_("Invalid user name or password.")
)
if domain != current_app.config["SNIKKET_DOMAIN"]:
# (a) prosody throws a 400 at us and I prefer to catch that here
# and (b) I dont want to pass on this obviously not-for-here
# password further than necessary.
form.password.errors.append(ERR_CREDENTIALS_INVALID)
else:
return redirect(url_for('user.index'))
jid = "{}@{}".format(localpart, domain)
password = form.password.data
try:
await client.login(jid, password)
except quart.exceptions.Unauthorized:
form.password.errors.append(ERR_CREDENTIALS_INVALID)
else:
await flash(
_("Login successful!"),
"success"
)
return redirect(url_for('user.index'))
return await render_template("login.html", form=form)
@bp.route("/meta/about.html")
async def about() -> str:
version = None
extra_versions = {}
if current_app.debug:
if current_app.debug or client.is_admin_session:
version = _version.version
extra_versions["Quart"] = quart.__version__
extra_versions["aiohttp"] = aiohttp.__version__
extra_versions["babel"] = babel.__version__
extra_versions["wtforms"] = wtforms.__version__
extra_versions["flask-wtf"] = flask_wtf.__version__
try:
extra_versions["Prosody"] = await client.get_server_version()
except quart.exceptions.Unauthorized:
extra_versions["Prosody"] = "unknown"
return await render_template(
"about.html",
version=_version.version,
version=version,
extra_versions=extra_versions,
)
@@ -105,6 +123,7 @@ def repad(s: str) -> str:
@bp.route("/avatar/<from_>/<code>")
async def avatar(from_: str, code: str) -> quart.Response:
etag: typing.Optional[str]
try:
etag = request.headers["if-none-match"]
except KeyError:
@@ -144,3 +163,8 @@ async def avatar(from_: str, code: str) -> quart.Response:
response.set_data(data)
return response
@bp.route("/_health")
async def health() -> Response:
return Response("STATUS OK", content_type="text/plain")

View File

@@ -44,6 +44,15 @@ class AdminUserInfo:
display_name: typing.Optional[str]
email: typing.Optional[str]
phone: typing.Optional[str]
roles: typing.Optional[typing.List[str]]
@property
def has_admin_role(self) -> bool:
return bool(self.roles and "prosody:admin" in self.roles)
@property
def has_restricted_role(self) -> bool:
return bool(self.roles and "prosody:restricted" in self.roles)
@classmethod
def from_api_response(
@@ -55,11 +64,13 @@ class AdminUserInfo:
display_name=data.get("display_name") or None,
email=data.get("email") or None,
phone=data.get("phone") or None,
roles=data.get("roles"),
)
class InviteType(enum.Enum):
REGISTER = "register"
ROSTER = "roster"
@dataclasses.dataclass(frozen=True)
@@ -100,6 +111,7 @@ class AdminInviteInfo:
class AdminGroupInfo:
id_: str
name: str
muc_jid: typing.Optional[str]
members: typing.Collection[str]
@classmethod
@@ -110,6 +122,7 @@ class AdminGroupInfo:
return cls(
id_=data["id"],
name=data["name"],
muc_jid=data.get("muc_jid") or None,
members=data.get("members", []),
)
@@ -118,6 +131,8 @@ class AdminGroupInfo:
class PublicInviteInfo:
inviter: typing.Optional[str]
xmpp_uri: str
reset_localpart: typing.Optional[str]
domain: str
@classmethod
def from_api_response(
@@ -127,6 +142,8 @@ class PublicInviteInfo:
return cls(
inviter=data.get("inviter") or None,
xmpp_uri=data["uri"],
reset_localpart=data.get("reset", None),
domain=data["domain"],
)
@@ -137,6 +154,7 @@ class HTTPSessionManager:
async def _create(self) -> aiohttp.ClientSession:
return aiohttp.ClientSession(headers={
"Accept": "application/json",
"Host": current_app.config["SNIKKET_DOMAIN"],
})
async def teardown(self, exc: typing.Optional[BaseException]) -> None:
@@ -197,6 +215,7 @@ class HTTPAuthSessionManager(HTTPSessionManager):
headers={
"Authorization": "Bearer {}".format(token),
"Accept": "application/json",
"Host": current_app.config["SNIKKET_DOMAIN"],
}
)
@@ -323,15 +342,18 @@ class ProsodyClient:
)
)
def _store_token_in_session(self, token_info: TokenInfo) -> None:
http_session[self.SESSION_TOKEN] = token_info.token
http_session[self.SESSION_CACHED_SCOPE] = " ".join(token_info.scopes)
async def login(self, jid: str, password: str) -> bool:
async with self._plain_session as session:
token_info = await self._oauth2_bearer_token(
session, jid, password,
)
http_session[self.SESSION_TOKEN] = token_info.token
self._store_token_in_session(token_info)
http_session[self.SESSION_ADDRESS] = jid
http_session[self.SESSION_CACHED_SCOPE] = " ".join(token_info.scopes)
return True
@property
@@ -436,6 +458,13 @@ class ProsodyClient:
headers=final_headers,
data=serialised) as resp:
if resp.status != 200:
self.logger.debug(
"IQ HTTP response (in-reply-to id=%s) with non-OK status "
"%s: %s",
id_,
resp.status,
resp.reason,
)
abort(resp.status)
reply_payload = await resp.read()
self.logger.debug(
@@ -481,9 +510,32 @@ class ProsodyClient:
"to": self.session_address,
}
async with session.post(self._rest_endpoint, data=req) as resp:
async with session.post(self._rest_endpoint, json=req) as resp:
return resp.status == 200
@autosession
async def get_server_version(self, session: aiohttp.ClientSession) -> str:
_, domain, _ = split_jid(self.session_address)
req = {
"kind": "iq",
"type": "get",
"version": {},
"to": domain,
}
async with session.post(self._rest_endpoint, json=req) as resp:
if resp.status != 200:
return "unknwn"
try:
return (await resp.json())["version"]["version"]
except Exception as exc:
self.logger.debug(
"failed to parse prosody version from response"
" (%s: %s)",
type(exc), exc,
)
return "unknown"
@autosession
async def get_user_nickname(
self,
@@ -758,7 +810,7 @@ class ProsodyClient:
# got there, replacing the current session token on the way.
async with self._plain_session as session:
token = await self._oauth2_bearer_token(
token_info = await self._oauth2_bearer_token(
session,
self.session_address,
current_password,
@@ -770,14 +822,14 @@ class ProsodyClient:
new_password
),
headers={
"Authorization": "Bearer {}".format(token),
"Authorization": "Bearer {}".format(token_info.token),
},
sensitive=True,
)
# TODO: error handling
# TODO: obtain a new token using the new password to allow the
# server to expire/revoke all tokens on password change.
http_session[self.SESSION_TOKEN] = token
self._store_token_in_session(token_info)
def _raise_error_from_response(
self,
@@ -816,6 +868,29 @@ class ProsodyClient:
self._raise_error_from_response(resp)
return AdminUserInfo.from_api_response(await resp.json())
@autosession
async def update_user(
self,
localpart: str,
*,
display_name: typing.Optional[str],
roles: typing.Optional[typing.Collection[str]],
session: aiohttp.ClientSession,
) -> None:
payload: typing.Dict[str, typing.Any] = {
"username": localpart,
}
if display_name is not None:
payload["display_name"] = display_name
if roles is not None:
payload["roles"] = list(roles)
async with session.put(
self._admin_v1_endpoint("/users/{}".format(localpart)),
json=payload,
) as resp:
self._raise_error_from_response(resp)
@autosession
async def get_user_debug_info(
self,
@@ -944,10 +1019,12 @@ class ProsodyClient:
self,
name: str,
*,
create_muc: bool = True,
session: aiohttp.ClientSession,
) -> AdminGroupInfo:
payload = {
"name": name,
"create_muc": create_muc,
}
async with session.post(
@@ -1098,3 +1175,41 @@ class ProsodyClient:
json=payload) as resp:
resp.raise_for_status()
return (await resp.json())["jid"]
@autosession
async def get_system_metrics(
self,
*,
session: aiohttp.ClientSession) -> typing.Mapping:
async with session.get(
self._admin_v1_endpoint("/server/metrics"),
) as resp:
if resp.status == 404:
return {}
self._raise_error_from_response(resp)
resp.raise_for_status()
return await resp.json()
@autosession
async def post_announcement(
self,
body: str,
recipients: str,
*,
session: aiohttp.ClientSession) -> None:
recipients_payload: typing.Union[str, typing.Sequence[str]]
if recipients == "self":
recipients_payload = [self.session_address]
else:
recipients_payload = recipients
payload = {
"recipients": recipients_payload,
"body": body,
}
async with session.post(
self._admin_v1_endpoint("/server/announcement"),
json=payload) as resp:
self._raise_error_from_response(resp)
resp.raise_for_status()

View File

@@ -11,15 +11,15 @@ $colours: (
#f6f5f4
],
"blue": [
#062943,
#0f3d62,
#0e4c76,
#226494,
#418fc7,
#72b2e3,
#9dccf0,
#b5d8f3,
#e4f3fd
#062243,
#0f3462,
#0e4276,
#225994,
#4182c7,
#72a7e3,
#9dc4f0,
#b5d2f3,
#e4f0fd
],
"red": [
#340e03,
@@ -39,20 +39,20 @@ $colours: (
#a07501,
#c79b0e,
#f4ce3f,
#feed93,
#fef6c1,
#fffbe8
#fee577,
#fef1c1,
#fff8e8
],
"green": [
#052f03,
#0c4608,
#197713,
#218a1b,
#55c644,
#81e06e,
#abed9c,
#cef6c5,
#ecfbe6
#172f03,
#244608,
#407713,
#548f19,
#7fc644,
#a1e06e,
#c0ed9c,
#dbf6c5,
#effbe6
]
);
@@ -171,6 +171,7 @@ $w-l4: 4rem;
$w-l5: 6rem;
$w-l6: 8rem;
$w-l7: 12rem;
$w-l8: 16rem;
$font-sans: "Noto Sans", sans-serif;
$font-serif: serif;
@@ -251,3 +252,4 @@ $h-sizes: [200.0%, 174.11011266%, 151.57165665%, 131.95079108%, 114.8698355%, 10
$h-small-sizes: [150.0%, 138.31618672%, 127.54245006%, 117.60790225%, 108.44717712%, 100.0%];
$small-screen-threshold: 40rem;
$medium-screen-threshold: 60rem;
$large-screen-threshold: 80rem;

View File

@@ -33,19 +33,41 @@ body {
main {
padding: $w-l1;
margin-left: auto;
max-width: 60rem;
margin-right: auto;
}
#mwrap {
flex: 1;
display: flex;
flex-direction: row-reverse;
> .filler, > .flashbox {
flex: 1 1 1rem;
}
> main {
flex: 0 1 60rem;
}
}
@media screen and (max-width: $large-screen-threshold) {
#mwrap {
display: block;
> main {
margin-left: auto;
margin-right: auto;
}
}
}
.flashbox > div.box > :first-child {
margin-top: 0;
}
/* top bar */
@mixin snikket-logo {
background-image: url('/static/img/snikket-logo.svg');
background-image: url('../img/snikket-logo.svg');
background-size: contain;
background-repeat: no-repeat;
background-position: $w-s2 0em;
@@ -67,6 +89,10 @@ div#topbar {
font-size: $_top-h-size;
line-height: 1.5;
body.debug & {
color: red;
}
@media screen and (max-width: $small-screen-threshold) {
font-size: $_top-h-small-size;
}
@@ -134,22 +160,20 @@ body > footer {
background-color: $gray-100;
color: $gray-800;
padding: 0 $w-l1;
font-size: 92.21079115%;
ul {
display: block;
padding: 0;
margin: 0;
list-style-type: none;
text-align: center;
line-height: 1.6267076567643135;
}
li {
display: inline-block;
margin: $w-l1 0;
}
li:before {
content: '';
padding-right: $w-s2;
display: block;
margin: $w-s1 0;
}
a, a:visited, a:hover, a:active, a:focus {
@@ -171,10 +195,10 @@ body > footer {
@for $n from 1 through 6 {
div.form h#{$n}.form-title {
font-size: 100%;
font-weight: bold;
line-height: 1.5;
margin-bottom: 1.5em;
font-size: nth($h-sizes, 4);
/* font-weight: bold; */
line-height: 1.5 / (nth($h-sizes, 4) / 100%);
margin: 1.5em / (nth($h-sizes, 4) / 100%) 0;
}
}
@@ -222,13 +246,27 @@ div.form {
}
div.form.layout-expanded {
label {
label, legend {
display: block;
font-weight: bold;
color: $gray-200;
}
fieldset {
display: block;
border: 0;
padding: 0;
margin: 0;
}
input[type="radio"] + label, input[type="checkbox"] + label {
font-weight: inherit;
color: inherit;
}
div.f-ebox {
margin-bottom: $w-l1;
margin: 1.5em 0;
line-height: 1.5;
}
div.f-bbox {
@@ -242,7 +280,6 @@ div.form.layout-expanded {
border: none;
border-bottom: $w-s4 solid $primary-500;
margin-bottom: -$w-s4;
padding: 0 $w-s3;
}
input[type=$type].has-error {
@@ -317,6 +354,15 @@ div.form.layout-expanded {
display: block;
}
.radio-button-ext > label > p {
margin-left: 1.75rem;
margin-top: 0;
}
.radio-button-ext > label .icon {
margin-left: 0.25em;
}
div.select-wrap {
display: block;
border-bottom: $w-s4 solid $primary-500;
@@ -374,10 +420,18 @@ div.form.layout-expanded {
}
}
div.avatar-wrap {
> .avatar {
margin: 0;
margin-right: $w-0;
}
}
textarea {
width: 100%;
border: none;
border-bottom: $w-s4 solid $primary-500;
line-height: 1.5;
}
textarea:hover {
@@ -389,11 +443,12 @@ div.form.layout-expanded {
}
}
.f-ebox > ul {
fieldset > ul {
/* radio group */
list-style-type: none;
margin: 0;
padding: 0;
padding: $w-s1 0;
padding-left: $w-l1;
> li {
margin: 0;
@@ -455,7 +510,7 @@ input[type="submit"], button, .button {
&.primary {
background: linear-gradient(0deg, $primary-500, $primary-600);
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
color: $primary-900;
color: white;
border: none;
/* TODO: fix vertical rhyhtm ... */
border-radius: $w-s4;
@@ -758,40 +813,10 @@ body#login {
}
/* welcome screen specials */
div.welcome-cards {
display: flex;
flex-wrap: wrap;
& > .card {
flex: 1 0 $w-l7;
margin: $w-s1;
@extend .el-2;
padding: $w-s1 $w-l1;
background: white;
}
& > a.card {
text-decoration: none;
color: inherit;
& > h2 {
color: $primary-200;
text-decoration: underline;
}
}
& > .card:hover, & > .card:active, & > .card:focus, & > .card:focus-within {
@extend .el-3;
}
}
/* admin area specials */
#topbar > div.admin-note {
color: $alert-500;
color: $alert-400;
font-size: nth($h-sizes, 5);
margin-left: $w-l1;
}
@@ -799,10 +824,14 @@ div.welcome-cards {
table {
border-collapse: collapse;
width: 100%;
}
td, th {
padding: $w-s1;
}
td, th {
padding: $w-s1;
}
th {
text-align: left;
}
div.elevated {
@@ -860,6 +889,104 @@ ul.inline {
}
/* welcome screen specials */
#home main {
> h1, > p {
text-align: center;
}
}
nav.welcome {
> ul {
display: flex;
flex-wrap: wrap;
list-style-type: none;
padding: 0;
justify-content: center;
> li {
@extend .el-3;
background-color: white;
flex: 1 0 $w-l7;
margin: $w-s1;
padding: $w-s1 $w-l1;
text-align: center;
max-width: $w-l8;
display: flex;
flex-direction: column;
justify-content: space-between;
&.wide {
flex: 1 0 auto;
/* display: block; */
}
.button {
display: block;
margin: $w-l1 0;
}
img {
display: block;
margin: $w-l1 $w-0;
--margin: $w-0 * 2;
width: calc(100% - var(--margin));
}
p {
margin-left: $w-0;
margin-right: $w-0;
}
}
}
}
div.profile-card {
display: flex;
flex-direction: row;
margin: $w-l1 0;
text-align: left;
> div.picture {
flex: 0 0 auto;
}
> div.details {
flex: 1 0 auto;
display: flex;
flex-direction: column;
> .display-name {
font-size: nth($h-small-sizes, 5);
line-height: 1.5 / (nth($h-small-sizes, 5) / 100%);
}
> .address {
display: flex;
flex-direction: row;
> input {
flex: 1 1 auto;
background-color: transparent;
border: none;
padding: 0;
margin: 0;
min-width: 0;
width: 0;
}
> .button {
flex: 0 0 auto;
margin: 0;
}
}
}
}
/* linearisation / responsive stuff */
@media screen and (max-width: $medium-screen-threshold) {
@@ -899,6 +1026,23 @@ ul.inline {
display: none;
}
}
input[type="submit"], button, .button {
&.slimmify {
> svg.icon {
margin-right: 0;
}
> span {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
top: -100px;
}
}
}
}
/* clipboard button */
@@ -952,6 +1096,12 @@ pre.guru-meditation {
}
}
body#login {
.form-title {
color: $primary-800;
}
}
body > footer {
background-color: $gray-200;
color: $gray-800;
@@ -999,14 +1149,10 @@ pre.guru-meditation {
}
}
div.welcome-cards {
& > .card {
background: black;
}
& > a.card {
& > h2 {
color: $primary-800;
nav.welcome {
> ul {
> li {
background-color: black;
}
}
}
@@ -1053,11 +1199,11 @@ pre.guru-meditation {
}
&.secondary {
background: linear-gradient(0deg, $gray-400, $gray-500);
background: linear-gradient(0deg, $gray-200, $gray-300);
color: $gray-900;
&:hover, &:focus {
background: linear-gradient(0deg, $gray-500, $gray-600);
background: linear-gradient(0deg, $gray-300, $gray-400);
color: white;
}
@@ -1120,3 +1266,52 @@ pre.guru-meditation {
color: $gray-700;
}
}
/* tooltip magic */
.with-tooltip {
position: relative;
text-decoration: underline;
text-decoration-style: dotted;
}
.with-tooltip:before {
content: attr(data-tooltip); /* here's the magic */
position: absolute;
font-size: 87.05505633%;
/* vertically center */
bottom: 100%;
transform: translateX(-50%);
left: 50%;
margin-bottom: $w-s2;
/* basic styles */
width:$w-l7;
padding: $w-s1;
background: black;
color: $gray-900;
text-align: center;
display: none;
}
.with-tooltip:after {
content: "";
position: absolute;
bottom: 100%;
transform: translateX(-50%);
left: 50%;
margin-bottom: -$w-s1;
/* the arrow */
border: 10px solid black;
border-color: black transparent transparent transparent;
display: none;
}
.with-tooltip:hover:before, .with-tooltip:hover:after {
display: block;
}

View File

@@ -54,6 +54,8 @@ div.install-buttons {
ul {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
list-style-type: none;
margin: $w-l1 0;
padding: 0;
@@ -74,6 +76,10 @@ img.play {
height: $w-l3;
}
img.fdroid {
height: $w-l3;
}
.tabbox {
display: flex;
flex-direction: column;
@@ -183,3 +189,13 @@ div.form.layout-expanded .lwrap {
background-attachment: fixed;
background-size: cover;
}
/* dark mode */
@media (prefers-color-scheme: dark) {
div.form.layout-expanded .lwrap {
span {
background: $gray-200;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -17,6 +17,11 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M9 16.2l-3.5-3.5c-.39-.39-1.01-.39-1.4 0-.39.39-.39 1.01 0 1.4l4.19 4.19c.39.39 1.02.39 1.41 0L20.3 7.7c.39-.39.39-1.01 0-1.4-.39-.39-1.01-.39-1.4 0L9 16.2z" />
</symbol>
<!-- from: action/delete/materialiconsround/24px.svg -->
<symbol id="icon-delete" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v10zM18 4h-2.5l-.71-.71c-.18-.18-.44-.29-.7-.29H9.91c-.26 0-.52.11-.7.29L8.5 4H6c-.55 0-1 .45-1 1s.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1z" />
</symbol>
<!-- from: action/logout/materialicons/24px.svg -->
<symbol id="icon-logout" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
@@ -32,6 +37,11 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M10.79 16.29c.39.39 1.02.39 1.41 0l3.59-3.59c.39-.39.39-1.02 0-1.41L12.2 7.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L12.67 11H4c-.55 0-1 .45-1 1s.45 1 1 1h8.67l-1.88 1.88c-.39.39-.38 1.03 0 1.41zM19 3H5c-1.11 0-2 .9-2 2v3c0 .55.45 1 1 1s1-.45 1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1v3c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
</symbol>
<!-- from: action/lock/materialiconsround/24px.svg -->
<symbol id="icon-lock" viewBox="0 0 24 24">
<g fill="none"><path d="M0 0h24v24H0V0z" /><path d="M0 0h24v24H0V0z" opacity=".87" /></g>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z" />
</symbol>
<!-- from: communication/qr_code/materialiconsround/24px.svg -->
<symbol id="icon-qrcode" viewBox="0 0 24 24">
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
@@ -42,6 +52,12 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M12.65 10C11.7 7.31 8.9 5.5 5.77 6.12c-2.29.46-4.15 2.29-4.63 4.58C.32 14.57 3.26 18 7 18c2.61 0 4.83-1.67 5.65-4H17v2c0 1.1.9 2 2 2s2-.9 2-2v-2c1.1 0 2-.9 2-2s-.9-2-2-2h-8.35zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
</symbol>
<!-- from: communication/rss_feed/materialiconsround/24px.svg -->
<symbol id="icon-broadcast" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<circle cx="6.18" cy="17.82" r="2.18" />
<path d="M5.59 10.23c-.84-.14-1.59.55-1.59 1.4 0 .71.53 1.28 1.23 1.4 2.92.51 5.22 2.82 5.74 5.74.12.7.69 1.23 1.4 1.23.85 0 1.54-.75 1.41-1.59-.68-4.2-3.99-7.51-8.19-8.18zm-.03-5.71C4.73 4.43 4 5.1 4 5.93c0 .73.55 1.33 1.27 1.4 6.01.6 10.79 5.38 11.39 11.39.07.73.67 1.28 1.4 1.28.84 0 1.5-.73 1.42-1.56-.73-7.34-6.57-13.19-13.92-13.92z" />
</symbol>
<!-- from: content/add_circle_outline/materialiconsround/24px.svg -->
<symbol id="icon-add" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
@@ -52,11 +68,6 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0" fill="none" />
<path d="M8 11h8v2H8zm12.1 1H22c0-2.76-2.24-5-5-5h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1zM3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM19 12h-2v3h-3v2h3v3h2v-3h3v-2h-3z" />
</symbol>
<!-- from: content/create/materialiconsround/24px.svg -->
<symbol id="icon-edit" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M3 17.46v3.04c0 .28.22.5.5.5h3.04c.13 0 .26-.05.35-.15L17.81 9.94l-3.75-3.75L3.15 17.1c-.1.1-.15.22-.15.36zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
</symbol>
<!-- from: content/remove_circle_outline/materialiconsround/24px.svg -->
<symbol id="icon-remove" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
@@ -72,6 +83,11 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M21.94 11.23C21.57 8.76 19.32 7 16.82 7h-2.87c-.52 0-.95.43-.95.95s.43.95.95.95h2.9c1.6 0 3.04 1.14 3.22 2.73.17 1.43-.64 2.69-1.85 3.22l1.4 1.4c1.63-1.02 2.64-2.91 2.32-5.02zM4.12 3.56c-.39-.39-1.02-.39-1.41 0s-.39 1.02 0 1.41l2.4 2.4c-1.94.8-3.27 2.77-3.09 5.04C2.23 15.05 4.59 17 7.23 17h2.82c.52 0 .95-.43.95-.95s-.43-.95-.95-.95H7.16c-1.63 0-3.1-1.19-3.25-2.82-.15-1.72 1.11-3.17 2.75-3.35l2.1 2.1c-.43.09-.76.46-.76.92v.1c0 .52.43.95.95.95h1.78L13 15.27V17h1.73l3.3 3.3c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L4.12 3.56zM16 11.95c0-.52-.43-.95-.95-.95h-.66l1.49 1.49c.07-.13.12-.28.12-.44v-.1z" />
</symbol>
<!-- from: content/send/materialiconsround/24px.svg -->
<symbol id="icon-send" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M3.4 20.4l17.45-7.48c.81-.35.81-1.49 0-1.84L3.4 3.6c-.66-.29-1.39.2-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z" />
</symbol>
<!-- from: navigation/arrow_back/materialiconsround/24px.svg -->
<symbol id="icon-back" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
@@ -97,14 +113,49 @@ licensed under the terms of the Apache 2.0 License -->
<rect fill="none" height="24" width="24" />
<g><path d="M12,12.75c1.63,0,3.07,0.39,4.24,0.9c1.08,0.48,1.76,1.56,1.76,2.73L18,17c0,0.55-0.45,1-1,1H7c-0.55,0-1-0.45-1-1l0-0.61 c0-1.18,0.68-2.26,1.76-2.73C8.93,13.14,10.37,12.75,12,12.75z M4,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2 C2,12.1,2.9,13,4,13z M5.13,14.1C4.76,14.04,4.39,14,4,14c-0.99,0-1.93,0.21-2.78,0.58C0.48,14.9,0,15.62,0,16.43L0,17 c0,0.55,0.45,1,1,1l3.5,0v-1.61C4.5,15.56,4.73,14.78,5.13,14.1z M20,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2 C18,12.1,18.9,13,20,13z M24,16.43c0-0.81-0.48-1.53-1.22-1.85C21.93,14.21,20.99,14,20,14c-0.39,0-0.76,0.04-1.13,0.1 c0.4,0.68,0.63,1.46,0.63,2.29V18l3.5,0c0.55,0,1-0.45,1-1L24,16.43z M12,6c1.66,0,3,1.34,3,3c0,1.66-1.34,3-3,3s-3-1.34-3-3 C9,7.34,10.34,6,12,6z" /></g>
</symbol>
<!-- from: social/people/materialiconsround/24px.svg -->
<symbol id="icon-people" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V18c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-1.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05.02.01.03.03.04.04 1.14.83 1.93 1.94 1.93 3.41V18c0 .35-.07.69-.18 1H22c.55 0 1-.45 1-1v-1.5c0-2.33-4.67-3.5-7-3.5z" />
</symbol>
<!-- from: social/group_add/materialiconsround/24px.svg -->
<symbol id="icon-create_group" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M7 10H5V8c0-.55-.45-1-1-1s-1 .45-1 1v2H1c-.55 0-1 .45-1 1s.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1zm11 1c1.66 0 2.99-1.34 2.99-3S19.66 5 18 5c-.32 0-.63.05-.91.14.57.81.9 1.79.9 2.86s-.34 2.04-.9 2.86c.28.09.59.14.91.14zm-5 0c1.66 0 2.99-1.34 2.99-3S14.66 5 13 5s-3 1.34-3 3 1.34 3 3 3zm0 2c-2 0-6 1-6 3v1c0 .55.45 1 1 1h10c.55 0 1-.45 1-1v-1c0-2-4-3-6-3zm6.62.16c.83.73 1.38 1.66 1.38 2.84v1.5c0 .17-.02.34-.05.5h2.55c.28 0 .5-.22.5-.5V16c0-1.54-2.37-2.49-4.38-2.84z" />
</symbol>
<!-- from: social/person_add/materialiconsround/24px.svg -->
<symbol id="icon-add_user" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V8c0-.55-.45-1-1-1s-1 .45-1 1v2H2c-.55 0-1 .45-1 1s.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1H6zm9 4c-2.67 0-8 1.34-8 4v1c0 .55.45 1 1 1h14c.55 0 1-.45 1-1v-1c0-2.66-5.33-4-8-4z" />
</symbol>
<!-- from: social/person_remove/materialiconsround/24px.svg -->
<symbol id="icon-remove_user" viewBox="0 0 24 24">
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
<g><path d="M14,8c0-2.21-1.79-4-4-4S6,5.79,6,8s1.79,4,4,4S14,10.21,14,8z M2,18v1c0,0.55,0.45,1,1,1h14c0.55,0,1-0.45,1-1v-1 c0-2.66-5.33-4-8-4S2,15.34,2,18z M18,10h4c0.55,0,1,0.45,1,1v0c0,0.55-0.45,1-1,1h-4c-0.55,0-1-0.45-1-1v0 C17,10.45,17.45,10,18,10z" /></g>
</symbol>
<!-- from: navigation/close/materialiconsround/24px.svg -->
<symbol id="icon-close" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z" />
</symbol>
<!-- from: image/edit/materialiconsround/24px.svg -->
<symbol id="icon-edit" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M3 17.46v3.04c0 .28.22.5.5.5h3.04c.13 0 .26-.05.35-.15L17.81 9.94l-3.75-3.75L3.15 17.1c-.1.1-.15.22-.15.36zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
</symbol>
<!-- from: action/admin_panel_settings/materialiconsround/24px.svg -->
<symbol id="icon-admin" viewBox="0 0 24 24">
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
<g><g><path d="M17,11c0.34,0,0.67,0.04,1,0.09V7.58c0-0.8-0.47-1.52-1.2-1.83l-5.5-2.4c-0.51-0.22-1.09-0.22-1.6,0l-5.5,2.4 C3.47,6.07,3,6.79,3,7.58v3.6c0,4.54,3.2,8.79,7.5,9.82c0.55-0.13,1.08-0.32,1.6-0.55C11.41,19.47,11,18.28,11,17 C11,13.69,13.69,11,17,11z" /><path d="M17,13c-2.21,0-4,1.79-4,4c0,2.21,1.79,4,4,4s4-1.79,4-4C21,14.79,19.21,13,17,13z M17,14.38c0.62,0,1.12,0.51,1.12,1.12 s-0.51,1.12-1.12,1.12s-1.12-0.51-1.12-1.12S16.38,14.38,17,14.38z M17,19.75c-0.93,0-1.74-0.46-2.24-1.17 c0.05-0.72,1.51-1.08,2.24-1.08s2.19,0.36,2.24,1.08C18.74,19.29,17.93,19.75,17,19.75z" /></g></g>
</symbol>
<!-- from: content/link/materialiconsround/24px.svg -->
<symbol id="icon-link" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M17 7h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c1.65 0 3 1.35 3 3s-1.35 3-3 3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-9 5c0 .55.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1H9c-.55 0-1 .45-1 1zm2 3H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h3c.55 0 1-.45 1-1s-.45-1-1-1H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h3c.55 0 1-.45 1-1s-.45-1-1-1z" />
</symbol>
<!-- from: content/insights/materialiconsround/24px.svg -->
<symbol id="icon-insights" viewBox="0 0 24 24">
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
<g><g><path d="M21,8c-1.45,0-2.26,1.44-1.93,2.51l-3.55,3.56c-0.3-0.09-0.74-0.09-1.04,0l-2.55-2.55C12.27,10.45,11.46,9,10,9 c-1.45,0-2.27,1.44-1.93,2.52l-4.56,4.55C2.44,15.74,1,16.55,1,18c0,1.1,0.9,2,2,2c1.45,0,2.26-1.44,1.93-2.51l4.55-4.56 c0.3,0.09,0.74,0.09,1.04,0l2.55,2.55C12.73,16.55,13.54,18,15,18c1.45,0,2.27-1.44,1.93-2.52l3.56-3.55 C21.56,12.26,23,11.45,23,10C23,8.9,22.1,8,21,8z" /><polygon points="15,9 15.94,6.93 18,6 15.94,5.07 15,3 14.08,5.07 12,6 14.08,6.93" /><polygon points="3.5,11 4,9 6,8.5 4,8 3.5,6 3,8 1,8.5 3,9" /></g></g>
</symbol>
</defs></svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="260.000000pt" height="260.000000pt" viewBox="0 0 260.000000 260.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,260.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1169 2496 c-2 -2 -24 -6 -49 -9 -51 -6 -196 -42 -237 -59 -360 -146
-607 -401 -723 -748 -49 -143 -62 -230 -62 -386 0 -177 27 -297 109 -498 115
-282 109 -516 -18 -660 l-30 -35 38 6 c151 22 316 93 400 172 12 12 25 21 28
21 3 0 41 -20 83 -45 181 -104 366 -152 592 -153 133 0 240 16 362 54 84 27
228 92 228 103 0 4 -14 15 -31 25 -254 150 -478 469 -534 761 -5 22 -10 47
-11 55 -21 97 -21 303 1 409 58 293 200 534 420 716 33 28 81 64 108 80 26 17
47 33 47 36 0 3 -39 24 -87 47 -86 40 -217 83 -298 97 -46 7 -329 17 -336 11z
m-117 -379 c90 -53 140 -137 140 -237 0 -231 -271 -353 -445 -199 -62 54 -89
113 -90 197 -1 125 70 223 193 263 49 16 155 4 202 -24z"/>
<path d="M847 2029 c-56 -29 -96 -101 -91 -161 12 -128 153 -196 261 -124 37
24 73 83 73 118 0 23 -4 25 -45 24 -75 -2 -108 46 -93 139 2 17 -3 20 -33 21
-20 1 -52 -7 -72 -17z"/>
<path d="M1885 2218 c-136 -100 -265 -251 -340 -398 -40 -80 -92 -223 -100
-281 -4 -24 -8 -46 -10 -49 -2 -3 -6 -35 -9 -71 l-6 -65 33 -1 c125 -3 325
-57 461 -124 77 -39 178 -101 204 -126 7 -7 17 -13 20 -13 14 0 178 -166 216
-219 39 -53 39 -53 52 -30 18 32 63 173 75 234 71 364 -31 723 -281 997 -64
70 -219 198 -238 198 -4 0 -38 -23 -77 -52z"/>
<path d="M1418 1245 c-7 -8 17 -171 37 -244 9 -35 28 -93 42 -128 l26 -64 41
7 c136 22 368 118 446 182 l31 27 -50 33 c-146 94 -330 161 -491 178 -36 3
-68 8 -71 10 -3 2 -8 1 -11 -1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -3,5 +3,7 @@
{#- -#}
<li>{% trans about_url=url_for('main.about') %}A <a href="{{ about_url }}">Snikket</a> service{% endtrans %}</li>
{#- -#}
<li>{% trans %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company.{% endtrans %}</li>
{#- -#}
</ul>
</footer>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% from "library.j2" import standard_button %}
{% block head_lead %}
<title>About Snikket</title>
<title>{% trans %}About Snikket{% endtrans %}</title>
{% endblock %}
{% block body %}
<main>
@@ -14,10 +14,12 @@
<p>{% trans agpl_url="https://www.gnu.org/licenses/agpl.html" %}The web portal software is licensed under the terms of the <a href="{{ agpl_url }}">Affero GNU General Public License, version 3.0 or later</a>. The full terms of the license can be reviewed using the aforementioned link.{% endtrans %}</p>
<p>{% trans source_url="https://github.com/snikket-im/snikket-web-portal/" %}The source code of the web portal can be downloaded and viewed in <a href="{{ source_url }}">its GitHub repository</a>.{% endtrans %}</p>
<p>{% trans source_url="https://material.io/resources/icons/", apache20_url="https://www.apache.org/licenses/LICENSE-2.0.txt" %}The icons used in the web portal are <a href="{{ source_url }}">Googles Material Icons</a>, made available by Google under the terms of the <a href="{{ apache20_url }}">Apache 2.0 License</a>.{% endtrans %}</p>
<h3>{% trans %}Trademarks{% endtrans %}</h3>
<p>{% trans trademarks_url="https://snikket.org/about/trademarks/" %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company. For more information about the trademarks, visit the <a href="{{ trademarks_url }}">Snikket Trademarks information page</a>.{% endtrans %}
<h3>{% trans %}Software Versions{% endtrans %}</h3>
<pre>Snikket Server
Domain: {{ config["SNIKKET_DOMAIN"] }}
Snikket Web Portal ({{ version }})
Snikket Web Portal{% if version %} ({{ version }}){% endif %}
{%- if extra_versions -%}
{% for name, version in extra_versions.items() %}
{{ name }} ({{ version }}){% endfor %}

View File

@@ -2,6 +2,8 @@
{% from "library.j2" import action_button, custom_form_button, form_button, circle_name %}
{% block content %}
<h1>{% trans %}Manage circles{% endtrans %}</h1>
<p>{% trans %}<em>Circles</em> aim to help people who are in the same social circle find each other on your service.{% endtrans %}</p>
<p>{% trans %}Users who are in the same circle will see each other in their contact list. In addition, each circle has a group chat where the circle members are included.{% endtrans %}</p>
{%- if circles -%}
<form method="POST" action="{{ url_for(".create_invite") }}">
{{- invite_form.csrf_token -}}
@@ -20,10 +22,13 @@
<td class="collapsible">{{ circle.members | length }}</td>
<td class="nowrap">
{%- call custom_form_button("create_link", invite_form.circles.name, circle.id_, slim=True, class="secondary accent") -%}
{% trans circle_name=circle.name %}Create invitation to circle {{ circle_name }}{% endtrans %}
{% trans circle_name=(circle | circle_name) %}Create invitation to circle {{ circle_name }}{% endtrans %}
{%- endcall -%}
{%- call action_button("more", url_for(".edit_circle", id_=circle.id_), class="primary") -%}
{% trans circle_name=circle.name %}Show details of circle {{ circle_name }}{% endtrans %}
{%- call action_button("people", url_for(".edit_circle", id_=circle.id_) + "#members", class="secondary") -%}
{% trans circle_name=(circle | circle_name) %}Manage members of {{ circle_name }}{% endtrans %}
{%- endcall -%}
{%- call action_button("edit", url_for(".edit_circle", id_=circle.id_), class="primary") -%}
{% trans circle_name=(circle | circle_name) %}Edit circle {{ circle_name }}{% endtrans %}
{%- endcall -%}
</td>
</tr>

View File

@@ -5,8 +5,10 @@
<h2 class="form-title">{% trans %}Create new invitation{% endtrans %}</h2>
<p class="form-descr weak">{% trans %}Create a new invitation link to invite more users to your Snikket service by clicking the button below.{% endtrans %}</p>
<div class="f-ebox">
{{ invite_form.reusable }}
{{ invite_form.reusable.label }}
<fieldset>{#- -#}
<legend>{{ invite_form.type_.label.text }}</legend>
{{- invite_form.type_ -}}
</fieldset>
</div>
<div class="f-ebox">
{{ invite_form.lifetime.label }}

View File

@@ -11,17 +11,13 @@
<dd>{{ target_user.localpart }}</dd>
<dt>{% trans %}Display name{% endtrans %}</dt>
<dd>{{ target_user.display_name }}</dd>
<dt>{% trans %}Email address{% endtrans %}</dt>
<dd>{{ target_user.email }}</dd>
<dt>{% trans %}Display name{% endtrans %}</dt>
<dd>{{ target_user.phone }}</dd>
</dl>
{% call box("alert", _("Danger")) %}
<p>{% trans %}The user and their data will be deleted irrevocably, permanently and immediately upon pushing the below button. <strong>There is no way back!</strong>{% endtrans %}</p>
{% endcall %}
<div class="f-bbox">
{%- call standard_button("back", url_for(".index"), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call form_button("remove", form.action_delete, class="primary danger") %}{% endcall -%}
{%- call standard_button("back", url_for(".edit_user", localpart=target_user.localpart), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call form_button("delete", form.action_delete, class="primary danger") %}{% endcall -%}
</div>
</form></div>
{% endblock %}

View File

@@ -1,42 +1,79 @@
{% extends "admin_app.html" %}
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button %}
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button, clipboard_button, icon %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
{% endblock %}
{% block content %}
<h1>{% trans circle_name=(target_circle | circle_name) %}Edit circle {{ circle_name }}{% endtrans %}</h1>
<form method="POST">
{{- form.csrf_token -}}
{%- if target_circle.id_ == "default" -%}
<input type="hidden" name="{{ form.name.name }}" value="{{ form.name.data }}">{#- -#}
<div class="box hint form layout-expanded">
<header>{% trans %}This is your main circle{% endtrans %}</header>
<p>{% trans %}This circle is managed automatically and cannot be removed or renamed.{% endtrans %}</p>
{%- if target_circle.muc_jid -%}
<div><label for="circle-muc-jid">{% trans %}Group chat address{% endtrans %}</label></div>
<div><input type="text" readonly="readonly" id="circle-muc-jid" value="{{ target_circle.muc_jid }}"></div>
{%- call clipboard_button(target_circle.muc_jid, show_label=True) -%}
{%- trans -%}Copy address{%- endtrans -%}
{%- endcall -%}
{%- endif -%}
</div>
{%- else -%}
<div class="form layout-expanded">
<h2 class="form-title">{% trans %}Circle information{% endtrans %}</h2>
<div class="f-ebox">
{{ form.name.label }}
{{ form.name }}
</div>
<div class="f-ebox">
{%- if target_circle.muc_jid -%}
<label for="circle-muc-jid">{% trans %}Group chat address{% endtrans %}</label>
<input type="text" readonly="readonly" id="circle-muc-jid" value="{{ target_circle.muc_jid }}">
{%- call clipboard_button(target_circle.muc_jid, show_label=True) -%}
{%- trans -%}Copy address{%- endtrans -%}
{%- endcall -%}
{%- else -%}
<p>{% trans %}This circle has no group chat associated.{% endtrans %}<p>
{%- endif -%}
</div>
<div class="f-bbox">
{%- call standard_button("back", url_for(".circles"), class="secondary") -%}
{% trans %}Back{% endtrans %}
{%- call standard_button("back", url_for(".circles"), class="tertiary") -%}
{% trans %}Return to circle list{% endtrans %}
{%- endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
</div>
<h3 class="form-title">{% trans %}Delete circle{% endtrans %}</h3>
<p class="form-desc">{% trans %}Deleting a circle does not delete any users in the circle.{% endtrans %}</p>
<div class="f-bbox">
{%- call form_button("done", form.action_delete, class="secondary danger") %}{% endcall -%}
{%- call form_button("delete", form.action_delete, class="secondary danger") %}{% endcall -%}
</div>
</div>
<h2>{% trans %}Circle members{% endtrans %}</h2>
{%- endif -%}
<h2 id="members">{% trans %}Circle members{% endtrans %}</h2>
{%- if circle_members -%}
<div class="el-2 elevated"><table>
<thead>
<th>Login name</th>
<th class="collapsible">Display name</th>
<th>Actions</th>
<th>{% trans %}Login name{% endtrans %}</th>
<th class="collapsible">{% trans %}Display name{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</thead>
<tbody>
{%- for member in circle_members -%}
{%- for localpart, member in circle_members -%}
<tr>
<td>{{ member.localpart }}</td>
<td>
{%- if member -%}
{{ localpart }}
{%- else -%}
{{ localpart }}
<span class="with-tooltip above" data-tooltip="{% trans %}The user has been deleted from the server.{% endtrans %}"><em> ({% trans %}deleted{% endtrans %})</em></span>
{%- endif -%}
</td>
<td class="collapsible">{% call value_or_hint(member.display_name) %}{% endcall %}</td>
<td class="nowrap">
{%- call custom_form_button("remove", form.action_remove_user.name, member.localpart, class="primary danger", slim=True) -%}
{%- call custom_form_button("remove_user", form.action_remove_user.name, member.localpart, class="primary danger", slim=True) -%}
{% trans username=member.localpart %}Remove user {{ username }} from circle{% endtrans %}
{%- endcall -%}
</td>
@@ -56,12 +93,12 @@
<div class="select-wrap">{{ form.user_to_add }}</div>
</div>
<div class="f-bbox">
{%- call form_button("add", form.action_add_user, class="primary") %}{% endcall -%}
{%- call form_button("add_user", form.action_add_user, class="primary") %}{% endcall -%}
</div>
</div>
{%- else -%}
<div class="box hint el-2">
<header>{% trans %}No users left{% endtrans %}</header>
<div class="elevated box success el-2">
<header>{% trans %}All users added{% endtrans %}</header>
<p>{% trans %}All users on this service are already in this circle.{% endtrans %}</p>
</div>
{%- endif -%}

View File

@@ -1,5 +1,5 @@
{% extends "admin_app.html" %}
{% from "library.j2" import showuri, form_button, standard_button, extract_circle_name %}
{% from "library.j2" import showuri, form_button, standard_button, extract_circle_name, invite_type_description %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
@@ -12,10 +12,10 @@
<dl>
<dt>{% trans %}Valid until{% endtrans %}</dt>
<dd>{{ invite.expires | format_date }}</dd>
<dt>{% trans %}Link{% endtrans %}</dt>
<dd>{% call showuri(invite.landing_page) %}{% endcall %}</dd>
<dt>{% trans %}Reusability{% endtrans %}</dt>
<dd>{% if invite.reusable %}{% trans %}This invitation link can be used arbitrarily often, until it expires, is revoked or a service-wide user limit is reached.{% endtrans %}{% else %}{% trans %}This invitation link can only be used once and is then depleted.{% endtrans %}{% endif %}</dd>
<dt><label for="link-field">{% trans %}Link{% endtrans %}</label></dt>
<dd>{% call showuri(invite.landing_page, id_="link-field") %}{% endcall %}</dd>
<dt>{% trans %}Invitation type{% endtrans %}</dt>
<dd>{% call invite_type_description(invite) %}{% endcall %}</dd>
{%- set ngroups = invite.group_ids | length -%}
{%- if ngroups > 1 -%}
{#- not supported via the web UI, but we should still display it properly -#}
@@ -36,14 +36,18 @@
{%- endif -%}
</dd>
{%- endif -%}
{%- if invite.type_.value == "roster" -%}
<dt>{% trans %}Contact{% endtrans %}</dt>
<dd>{% trans peer_jid=invite.jid %}The user will get added as contact of {{ peer_jid }}.{% endtrans %}</dd>
{%- endif -%}
<dt>{% trans %}Created{% endtrans %}</dt>
<dd>{{ invite.created_at | format_date }}</dd>
</dl>
<div class="f-bbox">
{%- call form_button("remove_link", form.action_revoke, class="secondary danger") %}{% endcall -%}
{%- call standard_button("back", url_for(".invitations"), class="primary") %}
{% trans %}Back{% endtrans %}
{%- call standard_button("back", url_for(".invitations"), class="tertiary") %}
{% trans %}Return to invitation list{% endtrans %}
{%- endcall %}
{%- call form_button("remove_link", form.action_revoke, class="primary danger") %}{% endcall -%}
</div>
</div>
</form>

View File

@@ -1,32 +1,78 @@
{% extends "admin_app.html" %}
{% from "library.j2" import box, form_button, standard_button, icon %}
{% macro access_level_description(role, caller=None) %}
{%- if role == "prosody:restricted" -%}
{% trans %}Limited users can interact with users on the same Snikket service and be members of circles.{% endtrans %}
{%- elif role == "prosody:normal" -%}
{% trans %}Like limited users and can also interact with users on other Snikket services.{% endtrans %}
{%- elif role == "prosody:admin" -%}
{% trans %}Like normal users and can access the admin panel in the web portal.{% endtrans %}
{%- endif -%}
{% endmacro %}
{% macro access_level_icon(role, caller=None) %}
{%- if role == "prosody:restricted" -%}
{% call icon("lock") %}{% endcall %}
{%- elif role == "prosody:admin" -%}
{% call icon("admin") %}{% endcall %}
{%- endif -%}
{% endmacro %}
{% block content %}
<h1>{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST">
<h2 class="form-title">{% trans %}User information{% endtrans %}</h2>
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
<h2 class="form-title">{% trans %}Edit user{% endtrans %}</h2>
<div class="f-ebox">
{{ form.localpart.label }}
{{ form.localpart(readonly="readonly") }}
<p class="form-desc weak">{% trans %}The login name cannot be changed.{% endtrans %}</p>
</div>
<div class="f-ebox">
{{ form.display_name.label }}
{{ form.display_name }}
</div>
<h3 class="form-title">{% trans %}Access Level{% endtrans %}</h3>
<p class="form-descr weak">{% trans %}The access level of a user determines what interactions are allowed for them on your Snikket service.{% endtrans %}</p>
<div class="f-ebox">
<fieldset>{#- -#}
<legend class="a11y-only">{{ form.role.label.text }}</legend>
{%- for level in form.role -%}
<div class="radio-button-ext">
{{ level }}<label for="{{ level.id }}">
{%- trans title=level.label.text, icon=access_level_icon(level.data), description=access_level_description(level.data) -%}
<strong>{{ title }}{{ icon }}</strong><p>{{ description }}</p>
{%- endtrans -%}
</label>
</div>
{%- endfor -%}
</fieldset>
</div>
<div class="f-bbox">
{%- call standard_button("back", url_for(".users"), class="tertiary") -%}
{%- trans -%}Return to user list{%- endtrans -%}
{%- endcall -%}
{%- call standard_button("delete", url_for(".delete_user", localpart=target_user.localpart), class="secondary") -%}
{%- trans -%}Delete user{%- endtrans -%}
{%- endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
</div>
</div>
<h2>{% trans %}Further actions{% endtrans %}</h2>
<div class="form layout-expanded">
<h2 class="form-title">{% trans %}Reset password{% endtrans %}</h2>
{{ form.csrf_token }}
<div class="f-ebox">
{{ form.username.label }}
{{ form.username(readonly="readonly") }}
<p class="form-desc">
{% trans %}If the user has lost their password, you can use the button below to create a special link which allows to change the password of the account, once.{% endtrans %}
</p>
<div class="f-bbox">
{%- call form_button("passwd", form.action_create_reset, class="primary") -%}{%- endcall -%}
</div>
<div class="f-ebox">
{{ form.nickname.label }}
{{ form.nickname(readonly="readonly") }}
<h2 class="form-title">{% trans %}Debug information{% endtrans %}</h2>
<p class="form-desc">
{% trans %}In some cases, extended information about the user account and the connected devices is necessary to troubleshoot issues. The button below reveals this (sensitive) information.{% endtrans %}
</p>
<div class="f-bbox">
{%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="primary") -%}
{%- trans -%}Show debug information{%- endtrans -%}
{%- endcall -%}
</div>
<div class="f-ebox">
{{ form.email.label }}
{{ form.email(readonly="readonly") }}
</div>
<div class="f-ebox">
{{ form.phone.label }}
{{ form.phone(readonly="readonly") }}
</div>
{{ form.action_save(class="primary") }}
<input type="submit" class="a11y-only">
<h2 class="form-title">{% trans %}Password reset{% endtrans %}</h2>
<p>{% trans %}If the user has forgotten their password, use the below button to create a password reset link. The password reset link can be used once to change the password of the account. Transmit the link to the user via a secure channel.{% endtrans %}</p>
{{ form.action_create_reset_link(class="secondary accent") }}
<h2 class="form-title">{% trans %}Delete user{% endtrans %}</h2>
<p>{% trans %}{% endtrans %}</p>
{{ form.action_create_reset_link(class="secondary accent") }}
</form></div>
</div></form>
{% endblock %}

View File

@@ -1,22 +1,55 @@
{% extends "admin_app.html" %}
{% set body_id = "home" %}
{% block content %}
<h1>{% trans %}Welcome to the administration dashboard!{% endtrans %}</h1>
<h1>{% trans %}Welcome to the admin panel!{% endtrans %}</h1>
<p>{% trans user_name=user_info.display_name %}At your service, {{ user_name }}.{% endtrans %}</p>
<div class="welcome-cards">
<a class="card" href="{{ url_for('.users') }}">
<h2>{% trans %}Manage users{% endtrans %}</h2>
<p>{% trans %}Modify administrative user information or delete users.{% endtrans %}</p>
</a>
<a class="card" href="{{ url_for('.circles') }}">
<h2>{% trans %}Manage circles{% endtrans %}</h2>
</a>
<a class="card" href="{{ url_for('.invitations') }}">
<h2>{% trans %}Manage invitations{% endtrans %}</h2>
<p>{% trans %}Create, revoke or view invitations.{% endtrans %}</p>
</a>
<a class="card" href="{{ url_for('user.index') }}">
<h2>{% trans %}Back to the main view{% endtrans %}</h2>
<p>{% trans %}Go back to your users web portal page.{% endtrans %}</p>
</a>
</div>
<nav class="welcome">
<ul>
<li>
<h2>{% trans %}Users{% endtrans %}</h2>
{#- -#}
<p>{% trans %}Create password reset links or delete users.{% endtrans %}</p>
{#- -#}
{# <img aria-hidden="true" src="{{ url_for("static", filename="img/illus-profile.svg") }}"> #}
{#- -#}
<div>{% call standard_button("people", url_for(".users"), class="primary") %}{% trans %}Manage users{% endtrans %}{% endcall %}</div>
{#- -#}
</li>
<li>
<h2>{% trans %}Circles{% endtrans %}</h2>
{#- -#}
<p>{% trans %}Create and manage social circles represented on your service.{% endtrans %}</p>
{#- -#}
<div>{% call standard_button("groups", url_for(".circles"), class="primary") %}{% trans %}Manage circles{% endtrans %}{% endcall %}</div>
{#- -#}
</li>
<li>
<h2>{% trans %}Invitations{% endtrans %}</h2>
{#- -#}
<p>{% trans %}Create, revoke or copy invitations.{% endtrans %}</p>
{#- -#}
<div>{% call standard_button("link", url_for(".invitations"), class="primary") %}{% trans %}Manage invitations{% endtrans %}{% endcall %}</div>
{#- -#}
</li>
<li>
<h2>{% trans %}System health{% endtrans %}</h2>
{#- -#}
{%- if show_metrics -%}
<p>{% trans %}View the server status or send a broadcast message to all users.{% endtrans %}</p>
{%- else -%}
<p>{% trans %}Send a broadcast message to all users.{% endtrans %}</p>
{%- endif -%}
{#- -#}
<div>{% call standard_button("insights", url_for(".system"), class="primary") %}{% trans %}Manage system{% endtrans %}{% endcall %}</div>
{#- -#}
</li>
<li>
{#- -#}
<p>{% trans %}Go back to your user's web portal page.{% endtrans %}</p>
{#- -#}
<div>{% call standard_button("logout", url_for("user.index"), class="secondary") %}{% trans %}Exit admin panel{% endtrans %}{% endcall %}</div>
{#- -#}
</li>
</ul>
</nav>
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends "admin_app.html" %}
{% from "library.j2" import action_button, icon, clipboard_button, form_button, custom_form_button, extract_circle_name %}
{% from "library.j2" import action_button, icon, clipboard_button, form_button, custom_form_button, extract_circle_name, invite_type_name, invite_type_description %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
@@ -18,8 +18,8 @@
<col/>
<thead>
<tr>
<th>{% trans %}Valid until{% endtrans %}</th>
<th class="collapsible">{% trans %}Reusable{% endtrans %}</th>
<th>{% trans %}Expires{% endtrans %}</th>
<th class="collapsible">{% trans %}Type{% endtrans %}</th>
<th class="collapsible">{% trans %}Circle{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
@@ -28,8 +28,7 @@
{% for invite in invites %}
<tr>
<td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</td>
<td class="collapsible">{% if invite.reusable %}{% trans %}Yes{% endtrans %}{% else %}{% trans
%}No{% endtrans %}{% endif %}</td>
<td class="collapsible"><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite) %}{% endcall %}">{% call invite_type_name(invite) %}{% endcall %}</span></td>
<td class="collapsible">
{#- -#}
<ul class="inline">

View File

@@ -9,19 +9,19 @@
<form method="POST">
{{- form.csrf_token -}}
<div class="form layout-expanded">
<h2 class="form-title">{% trans user_name=target_user.localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2>
<h2 class="form-title">{% trans user_name=localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2>
<p class="form-desc">{% trans %}The following link will allow the user to reset their password on their device, once.{% endtrans %}</p>
<dd>
<dt>{% trans %}Valid until{% endtrans %}</dt>
<dd>{{ reset_link.expires | format_date }}</dd>
<dt>{% trans %}Link{% endtrans %}</dt>
<dd>{% call showuri(reset_link.landing_page) %}{% endcall %}</dd>
<dt><label for="link-field">{% trans %}Link{% endtrans %}</label></dt>
<dd>{% call showuri(reset_link.landing_page, id_="link-field") %}{% endcall %}</dd>
</dd>
<div class="f-bbox">
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}
{% trans %}Destroy link{% endtrans %}
{%- endcall -%}
{%- call standard_button("back", url_for(".users"), class="primary") -%}
{%- call standard_button("back", url_for(".edit_user", localpart=localpart), class="primary") -%}
{% trans %}Back{% endtrans %}
{%- endcall -%}
</div>

View File

@@ -0,0 +1,97 @@
{% extends "admin_app.html" %}
{% from "library.j2" import form_button %}
{% block content %}
<h1>{% trans %}Manage system{% endtrans %}</h1>
{% if show_metrics %}
<h2>{% trans %}Overall system status{% endtrans %}</h2>
<div class="elevated el-2">
<dl>
<dt>{% trans %}System load (5 minute average){% endtrans %}</dt>
<dd>
{%- if metrics.load5 -%}
{{ metrics.load5 }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Memory use{% endtrans %}</dt>
<dd>
{%- if metrics.mem_total and metrics.mem_available -%}
{% trans percentage_global=((1 - (metrics.mem_available / metrics.mem_total)) | format_percent), percentage_snikket=((((metrics.prosody_rss | default(0)) + (metrics.portal_rss | default(0))) / metrics.mem_total) | format_percent), mem_available=(metrics.mem_total | format_bytes) %}{{ percentage_global }} of {{ mem_available }}. Of that, Snikket uses {{ percentage_snikket }}.{% endtrans %}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
</dl>
</div>
<h2>{% trans %}Web portal status{% endtrans %}</h2>
<div class="elevated el-2">
<dl>
<dt>{% trans %}Version{% endtrans %}</dt>
<dd>{{ version }} <a href="{{ url_for("main.about") }}">{% trans %}View all versions{% endtrans %}</a></dd>
<dt>{% trans %}Average CPU use{% endtrans %}</dt>
<dd>
{%- if metrics.portal_cpu -%}
{{ metrics.portal_cpu | format_percent }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Current memory use{% endtrans %}</dt>
<dd>
{%- if metrics.portal_rss -%}
{{ metrics.portal_rss | format_bytes }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
</dl>
</div>
<h2>{% trans %}Snikket server status{% endtrans %}</h2>
<div class="elevated el-2">
<dl>
<dt>{% trans %}Version{% endtrans %}</dt>
<dd>{{ prosody_version }} <a href="{{ url_for("main.about") }}">{% trans %}View all versions{% endtrans %}</a></dd>
<dt>{% trans %}Average CPU use{% endtrans %}</dt>
<dd>
{%- if metrics.prosody_cpu -%}
{{ metrics.prosody_cpu | format_percent }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Current memory use{% endtrans %}</dt>
<dd>
{%- if metrics.prosody_rss -%}
{{ metrics.prosody_rss | format_bytes }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Connected devices{% endtrans %}</dt>
<dd>
{%- if metrics.prosody_devices | default(None) is not none -%}
{{ metrics.prosody_devices }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
</dl>
</div>
{% endif %}
<h2>{% trans %}Broadcast message{% endtrans %}</h2>
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
<p class="form-desc">{% trans %}This form allows you to send a message to all users currently online on your Snikket server. Use it wisely.{% endtrans %}</p>
<div class="f-ebox">
{{ form.text.label }}
{{ form.text }}
</div>
<div class="f-ebox">
{{ form.online_only }}{{ form.online_only.label }}
</div>
<div class="f-bbox">
{%- call form_button("send", form.action_send_preview, class="primary") -%}{%- endcall -%}
{%- call form_button("broadcast", form.action_post_all, class="secondary accent") -%}{%- endcall -%}
</div>
</div></form>
{% endblock %}

View File

@@ -1,35 +1,31 @@
{% extends "admin_app.html" %}
{% from "library.j2" import action_button, value_or_hint, custom_form_button %}
{% from "library.j2" import action_button, icon, value_or_hint, custom_form_button %}
{% block content %}
<h1>{% trans %}Manage users{% endtrans %}</h1>
<form method="POST" action="{{ url_for(".create_password_reset_link") }}">
{{- reset_form.csrf_token -}}
<div class="elevated el-2"><table>
<thead>
<tr>
<th>{% trans %}Login name{% endtrans %}</th>
<th>{% trans %}Display name{% endtrans %}</th>
<th class="collapsible">{% trans %}Email address{% endtrans %}</th>
<th class="collapsible">{% trans %}Phone number{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.localpart }}</td>
<td>
{{- user.localpart -}}
{%- if user.has_admin_role -%}
<span class="with-tooltip above" data-tooltip="{% trans %}The user is an administrator.{% endtrans %}">{% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %}</span>
{%- endif -%}
{%- if user.has_restricted_role -%}
<span class="with-tooltip above" data-tooltip="{% trans %}The user is restricted.{% endtrans %}">{% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %}</span>
{%- endif -%}
</td>
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
<td class="collapsible">{% call value_or_hint(user.email) %}{% endcall %}</td>
<td class="collapsible">{% call value_or_hint(user.phone) %}{% endcall %}</td>
<td class="nowrap">
{%- call action_button("remove", url_for(".delete_user", localpart=user.localpart), class="secondary") -%}
{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %}
{%- endcall -%}
{%- call action_button("bug_report", url_for(".debug_user", localpart=user.localpart), class="secondary") -%}
{% trans user_name=user.localpart %}Show debug information for {{ user_name }}{% endtrans %}
{%- endcall -%}
{%- call custom_form_button("create_link", reset_form.action_create.name, user.localpart, class="secondary", slim=True) -%}
{% trans user_name=user.localpart %}Create password reset link for {{ user_name }}{% endtrans %}
{%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%}
{% trans user_name=user.localpart %}Edit user {{ user_name }}{% endtrans %}
{%- endcall -%}
</form>
</td>
@@ -37,5 +33,5 @@
{% endfor %}
</tbody>
</table></div>
</form>
{%- include "admin_create_invite_form.html" -%}
{% endblock %}

View File

@@ -1,9 +1,9 @@
{% extends "unauth.html" %}
{% from "library.j2" import avatar with context %}
{% from "library.j2" import standard_button %}
{% block head_lead %}
<title>{% trans %}Snikket Web Portal{% endtrans %}</title>
{% endblock %}
{% block topbar_right %}
{{- super() -}}
<nav class="usermenu">{{ user_info.display_name }}{% call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall %}</nav>
{% call standard_button("logout", url_for("user.logout"), class="tertiary slimmify") %}{% trans %}Log out{% endtrans %}{% endcall %}
{%- endblock %}

View File

@@ -8,13 +8,13 @@
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/common.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/app.css') }}">
{% endblock %}
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for("static", filename="img/apple-touch-icon.png") }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for("static", filename="img/favicon-32x32.png") }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for("static", filename="img/favicon-16x16.png") }}">
<link rel="manifest" href="{{ url_for("site_manifest") }}">
<link rel="mask-icon" href="{{ url_for("static", filename="img/safari-pinned-tab.svg") }}" color="#5bbad5">
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
</head>
<body{% if body_id | default(False) %} id="{{ body_id }}"{% endif %}{% if body_class | default(False) %} class="{{ body_class }}"{% endif %}{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
<body{% if body_id | default(False) %} id="{{ body_id }}"{% endif %} class="{% if is_in_debug_mode %}debug{% endif %}{% if body_class | default(False) %} {{ body_class }}{% endif %}"{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
</html>

View File

@@ -10,7 +10,7 @@
{% block body %}
<h1 id="dummy">Theme Demo</h1>
<p>This page is to demonstrate the Snikket Web Portal theme and allow development. You should not see this during normal use.</p>
<p><a id="disable-lines" href="#no-lines" class="button secondary">Disable rhythm lines</a><a id="enable-lines" href="#dummy" class="button secondary">Enable rhythm lines</a></p>
<p style="height: 3rem"><a id="disable-lines" href="#no-lines" class="button secondary">Disable rhythm lines</a><a id="enable-lines" href="#dummy" class="button secondary">Enable rhythm lines</a></p>
<h2>Headings</h2>
<p>This subsection is responsible for demonstrating the heading sizes, with the relation between the different headings and also the relation between headings and text.</p>
<div class="demo-columns"><div class="demo-column">
@@ -145,14 +145,20 @@
<input type="text" id="fex-f1" name="fex-f1">
</div>
<div class="f-ebox">
<input type="checkbox" id="fex-f2" name="fex-f2"><label for="fex-f2">Enable fancy features</label>
<input type="checkbox" id="fex-f3" name="fex-f3"><label for="fex-f3">Enable more features</label>
<input type="checkbox" id="fex-f4" name="fex-f4"><label for="fex-f4">Also do that other thing</label>
<label>Optional features</label>
<ul id="check-features">
<li><input type="checkbox" id="fex-f2" name="fex-f2"><label for="fex-f2">Enable fancy features</label></li>
<li><input type="checkbox" id="fex-f3" name="fex-f3"><label for="fex-f3">Enable more features</label></li>
<li><input type="checkbox" id="fex-f4" name="fex-f4"><label for="fex-f4">Also do that other thing</label></li>
</ul>
</div>
<div class="f-ebox">
<input type="radio" id="fex-f5" name="fex-rg1"><label for="fex-f5">High difficulty</label>
<input type="radio" id="fex-f6" name="fex-rg1"><label for="fex-f6">Medium difficulty</label>
<input type="radio" id="fex-f7" name="fex-rg1"><label for="fex-f7">Low difficulty</label>
<label>Level</label>
<ul id="radio-difficulty">
<li><input type="radio" id="fex-f5" name="fex-rg1"><label for="fex-f5">High difficulty</label></li>
<li><input type="radio" id="fex-f6" name="fex-rg1"><label for="fex-f6">Medium difficulty</label></li>
<li><input type="radio" id="fex-f7" name="fex-rg1"><label for="fex-f7">Low difficulty</label></li>
</ul>
</div>
<div class="f-ebox">
<label for="fex-f8">Select dropdown:</label>

View File

@@ -5,6 +5,6 @@
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="css/invite.css") }}">
{% endblock %}
{% block body %}
<div id="mwrap"><main>{% block content %}{% endblock %}</main></div>
<div id="mwrap"><div class="filler"></div><main>{% block content %}{% endblock %}</main><div class="filler"></div></div>
{%- include "_footer.html" -%}
{% endblock %}

View File

@@ -1,11 +1,12 @@
{% extends "invite.html" %}
{% set body_id = "invite" %}
{% block content %}
<div class="elevated box el-3">
<h1>{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }}{% endtrans %}</h1>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo-text.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
<div class="box alert">
<header>{% trans %}Invite expired{% endtrans %}</header>
<p>{% trans %}Sorry, it looks like this invite code has expired!{% endtrans %}</p>
<p>{% trans %}Sorry, it looks like this invitation link has expired!{% endtrans %}</p>
</div>
<img alt="Sad person holding empty box" src="{{ url_for("static", filename="img/illus-empty.svg") }}" class="fullwidth">
</div>

View File

@@ -7,7 +7,7 @@
{% block content %}
<div class="elevated box el-3">
<h1>{% trans site_name=config["SITE_NAME"] %}Register on {{ site_name }}{% endtrans %}</h1>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo-text.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
<p>{% trans site_name=config["SITE_NAME"] %}{{ site_name }} is using Snikket - a secure, privacy-friendly chat app.{% endtrans %}</p>
<h2>{% trans %}Create an account{% endtrans %}</h2>
<p>{% 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 %}</p>

View File

@@ -0,0 +1,35 @@
{% extends "unauth.html" %}
{% from "library.j2" import standard_button, render_errors %}
{% block style %}
{{ super() }}
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="css/invite.css") }}">
{% endblock %}
{% block head_lead %}
{{ super() }}
<title>{% trans %}Reset your password | Snikket{% endtrans %}</title>
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
{% endblock %}
{% block content %}
<form method="POST"><div class="form layout-expanded">
{{- form.csrf_token -}}
<h1 class="form-title">{% trans %}Reset your password online{% endtrans %}</h1>
<p class="form-desc weak">{% trans %}To reset your password online, fill out the fields below and confirm using the button.{% endtrans %}</p>
{%- call render_errors(form) %}{% endcall -%}
<div class="f-ebox">
{{ form.password.label }}
{{ form.password }}
</div>
<div class="f-ebox">
{{ form.password_confirm.label }}
{{ form.password_confirm }}
</div>
<div class="f-bbox">
{%- call form_button("passwd", form.action_reset, class="primary") -%}{%- endcall -%}
</div>
</div></form>
<script type="text/javascript">
var onload = function() {
apply_qr_code(document.getElementById("qr-uri"));
};
</script>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "unauth.html" %}
{% from "library.j2" import standard_button %}
{% block head_lead %}
{{ super() }}
<title>{% trans %}Password reset successful | Snikket{% endtrans %}</title>
{% endblock %}
{% block content %}
<h1>{% trans %}Password reset successful{% endtrans %}</h1>
<div class="box success">
<header>{% trans %}Your password has been changed{% endtrans %}</header>
<p>{% trans %}You can now log in using your new password.{% endtrans %}</p>
<p>{% trans %}You can now safely close this page.{% endtrans %}</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends "unauth.html" %}
{% from "library.j2" import standard_button %}
{% block style %}
{{ super() }}
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="css/invite.css") }}">
{% endblock %}
{% block head_lead %}
{{ super() }}
<title>{% trans %}Reset your password | Snikket{% endtrans %}</title>
<script async type="text/javascript" src="{{ url_for("static", filename="js/invite-magic.js") }}"></script>
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
{% endblock %}
{% block content %}
<h1>{% trans %}Reset your password{% endtrans %}</h1>
<p>{% trans account_jid=account_jid %}This page allows you to reset the password of your account, <strong>{{ account_jid }}</strong>, once.{% endtrans %}</p>
<div class="elevated el-2">
<h2>{% trans %}Using the app{% endtrans %}</h2>
<p>{% trans %}To reset your password using the Snikket App, tap the button below.{% endtrans %}</p>
<div>
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="secondary") -%}
{% trans %}Open the app{% endtrans %}
{%- endcall -%}
</div>
<img class="float-right" id="tutorial-scan" aria-hidden="true" alt="" src="{{ url_for("static", filename="img/tutorial-scan.png") }}">
<p>{% trans %}Alternatively, you can scan the below code with the Snikket App using the Scan button at the top.{% endtrans %}</p>
<p>{% trans %}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.{% endtrans %}</p>
<p>{% trans %}You will then be prompted to enter a new password for your account.{% endtrans %}</p>
<div id="qr-uri" data-qrdata="{{ invite.xmpp_uri }}" class="qr"></div>
<h2>{% trans %}Alternatives{% endtrans %}</h2>
<p>{% trans reset_url=url_for(".reset", id_=invite_id) %}You can also <a href="{{ reset_url }}">reset your password online</a> if the above button or scanning the QR code does not work for you.{% endtrans %}</p>
</div>
<script type="text/javascript">
var onload = function() {
apply_qr_code(document.getElementById("qr-uri"));
};
</script>
{% endblock %}

View File

@@ -8,13 +8,13 @@
{% block content %}
<div class="elevated box el-3 form layout-expanded">
<h1>{% trans site_name=config["SITE_NAME"] %}Successfully registered on {{ site_name }}{% endtrans %}</h1>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo-text.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
<p>{% trans site_name=config["SITE_NAME"], jid=jid %}Congratulations! You successfully registered on {{ site_name }} as {{ jid }}.{% endtrans %}</p>
<input type="text" readonly="readonly" value="{{ jid }}">
<label for="address" class="a11y-only">{% trans %}Your address{% endtrans %}</label><input type="text" readonly="readonly" value="{{ jid }}" id="address">
{%- call clipboard_button(jid, show_label=True) -%}
{% trans %}Copy address{% endtrans %}
{%- endcall -%}
<p>{% trans %}You can not set up your legacy XMPP client with the above address and the password you chose during registration.{% endtrans %}</p>
<p>{% trans %}You can now safely close this page.{% endtrans %}</p>
<p>{% trans %}You can now set up your legacy XMPP client with the above address and the password you chose during registration.{% endtrans %}</p>
<p>{% trans login_url=url_for('main.login') %}You can now safely close this page, or log in to the web portal to <a href="{{ login_url }}">manage your account</a>.{% endtrans %}</p>
</div>
{% endblock %}

View File

@@ -6,25 +6,33 @@
<title>{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }} | Snikket{% endtrans %}</title>
<script async type="text/javascript" src="{{ url_for("static", filename="js/invite-magic.js") }}"></script>
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
<link rel="alternate" href="{{ invite.xmpp_uri }}">
{% endblock %}
{% block content %}
<div class="elevated box el-3">
<h1>{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }}{% endtrans %}</h1>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo-text.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
{%- if invite.inviter -%}
<p>{% 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 %}</p>
{%- else -%}
<p>{% trans site_name=config["SITE_NAME"] %}You have been invited to chat on {{ site_name }} using Snikket, a secure, privacy-friendly chat app.{% endtrans %}</p>
{%- endif -%}
<h2>{% trans %}Get started{% endtrans %}</h2>
{%- if apple_store_url -%}
<p>{% trans %}Install the Snikket App on your Android or iOS device.{% endtrans %}</p>
{%- else -%}
<p>{% trans ios_info_url="https://snikket.org/faq/#is-there-an-ios-app" %}Install the Snikket App on your Android device (<a href="{{ ios_info_url }}" rel="noopener noreferrer" target="_blank">iOS coming soon!</a>).{% endtrans %}</p>
{%- endif -%}
<div class="install-buttons">
<ul>
<li><a href="{{ play_store_url }}"><img alt='{% trans %}Get it on Google Play{% endtrans %}' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' class="play"/></a></li>
<li><a href="{{ apple_store_url }}"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></li>
{%- if apple_store_url -%}
<li><a href="{{ apple_store_url }}" class="popover" data-popover-id="apple-popover"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></li>
{%- endif -%}
<li><a href="{{ f_droid_url }}" class="popover" data-popover-id="fdroid-popover"><img alt='{% trans %}Get it on F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></li>
</ul>
{%- call standard_button("qrcode", "#qr-modal", class="primary", onclick="open_modal(this); return false;") -%}
{% trans %}Not on mobile?{% endtrans %}
{% trans %}Send to mobile device{% endtrans %}
{%- endcall -%}
</div>
<p>{% trans %}After installation the app should automatically open and prompt you to create an account. If not, simply click the button below.{% endtrans %}</p>
@@ -60,7 +68,7 @@
{#- -#}
<div id="qr-info-url" class="tab-pane active">
<p>{% trans %}Use a <em>QR code</em> scanner on your mobile device to scan the code below:{% endtrans %}</p>
<div id="qr-invite-page" data-qrdata="{{ url_for(".view", id_=invite_id, _external=True) }}" class="qr"></div>
<div id="qr-invite-page" data-qrdata="{{ url_for(".view", id_=invite_id, _external=True, _scheme="https") }}" class="qr"></div>
</div>
{#- -#}
<div id="qr-info-uri" class="tab-pane">
@@ -77,10 +85,77 @@
{%- endcall -%}
</div>
</div>
{%- if apple_store_url -%}
<div id="apple-popover" class="modal" tabindex="-1" role="dialog" aria-hidden="true" style="display: none;" onclick="close_modal(this); return false;">
<div role="document" class="elevated box el-2" onclick="event.stopPropagation();">
<header class="modal-title">
{#- -#}
<span>{% trans %}Install on iOS{% endtrans %}</span>
{#- -#}
{%- call action_button("close", "#", onclick="close_modal(this.parentNode.parentNode.parentNode); return false;", class="tertiary") -%}
{% trans %}Close{% endtrans %}
{%- endcall -%}
</header>
<p>{% trans %}After downloading Snikket from the App Store, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
<ol>
<li><p>{% trans %}First download Snikket from the App Store using the button below:{% endtrans %}</p>
<p><a href="{{ apple_store_url }}"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></p>
<li><p>{% trans %}After the installation is complete, you can return to this page and tap the "Open the app" button to continue with the setup:{% endtrans %}</p>
<p>
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="primary") -%}
{% trans %}Open the app{% endtrans %}
{%- endcall -%}
</p></li>
</ol>
{#- -#}
{%- call standard_button("close", "#", onclick="close_modal(this.parentNode.parentNode); return false;", class="secondary") -%}
{% trans %}Close{% endtrans %}
{%- endcall -%}
</div>
</div>
{%- endif -%}
<div id="fdroid-popover" class="modal" tabindex="-1" role="dialog" aria-hidden="true" style="display: none;" onclick="close_modal(this); return false;">
<div role="document" class="elevated box el-2" onclick="event.stopPropagation();">
<header class="modal-title">
{#- -#}
<span>{% trans %}Install via F-Droid{% endtrans %}</span>
{#- -#}
{%- call action_button("close", "#", onclick="close_modal(this.parentNode.parentNode.parentNode); return false;", class="tertiary") -%}
{% trans %}Close{% endtrans %}
{%- endcall -%}
</header>
<p>{% trans %}After installing Snikket via F-Droid, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
<ol>
<li><p>{% trans %}First install Snikket from F-Droid using the button below:{% endtrans %}</p>
<p><a href="{{ f_droid_url }}"><img alt='{% trans %}Install via F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></p></li>
<li><p>{% trans %}After the installation is complete, you can return to this page and tap the "Open the app" button to continue with the setup:{% endtrans %}</p>
<p>
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="primary") -%}
{% trans %}Open the app{% endtrans %}
{%- endcall -%}
</p></li>
</ol>
{#- -#}
{%- call standard_button("close", "#", onclick="close_modal(this.parentNode.parentNode); return false;", class="secondary") -%}
{% trans %}Close{% endtrans %}
{%- endcall -%}
</div>
</div>
<script type="text/javascript">
var catch_popover = function() {
open_modal(this);
return false;
}
var onload = function() {
apply_qr_code(document.getElementById("qr-invite-page"));
apply_qr_code(document.getElementById("qr-uri"));
var popover_as = document.getElementsByClassName("popover");
for (var i = 0; i < popover_as.length; ++i) {
var a = popover_as[i];
a.onclick = catch_popover;
a.href = "#" + a.dataset.popoverId;
}
};
</script>
{% endblock %}

View File

@@ -10,11 +10,11 @@
{%- endif -%}
{%- endmacro %}
{% macro showuri(uri, caller=None) %}
{% macro showuri(uri, caller=None, id_=None) %}
{%- if uri is none -%}
<em>—</em>
{%- else -%}
<div><input type="text" readonly="readonly" value="{{ uri }}"></div>
<div><input type="text" {% if id_ %}id="{{ id_ }}" {% endif %}readonly="readonly" value="{{ uri }}"></div>
<div>{% call clipboard_button(uri, show_label=True) %}{% trans %}Copy link{% endtrans %}{% endcall %}</div>
{%- endif -%}
{% endmacro %}
@@ -26,7 +26,7 @@
{% macro standard_button(icon_name, href, caller=None, class=None, onclick=None) -%}
{%- set label = caller() -%}
<a href="{{ href }}" class="button {% if class %}{{ class }}{% endif %}" aria-label="{{ a11y }}" title="{{ a11y }}"{% if onclick %} onclick="{{ onclick }}"{% endif %}>{% call icon(icon_name) %}{% endcall %}<span>{{ label }}</span></a>
<a href="{{ href }}" class="button {% if class %}{{ class }}{% endif %}" {% if onclick %} onclick="{{ onclick }}"{% endif %}>{% call icon(icon_name) %}{% endcall %}<span>{{ label }}</span></a>
{%- endmacro %}
{% macro form_button(icon_name, button_obj, caller=None, class=None) -%}
@@ -60,6 +60,7 @@
{% macro clipboard_button(data, show_label=False, caller=None, class=None) -%}
{%- set label = caller() -%}
<a class="button{% if class %} {{ class }}{% endif %}"
href="#"
{% if not show_label %}
aria-label="{{ label }}"
title="{{ label }}"
@@ -79,7 +80,7 @@
<div class="box warning">{#- -#}
<header>{% trans %}Invalid input{% endtrans %}</header>
{%- if error_list | length == 1 -%}
<p>{{ error_list[0] }}.</p>
<p>{{ error_list[0] }}</p>
{%- else -%}
<ul>
{%- for error in error_list -%}
@@ -107,3 +108,19 @@
<em>{% trans %}deleted{% endtrans %}</em>
{%- endif -%}
{% endmacro %}
{%- macro invite_type_name(invite_info, caller=None) -%}
{%- if invite_info.reusable -%}
{% trans %}Group{% endtrans %}
{%- else -%}
{% trans %}Individual{% endtrans %}
{%- endif -%}
{%- endmacro -%}
{%- macro invite_type_description(invite_info, caller=None) -%}
{%- if invite_info.reusable -%}
{% trans %}Can be used multiple times to create accounts on this Snikket service.{% endtrans %}
{%- else -%}
{% trans %}Can be used once to create an account on this Snikket service.{% endtrans %}
{%- endif -%}
{%- endmacro -%}

View File

@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% from "library.j2" import box, form_button %}
{% from "library.j2" import box, form_button, render_errors %}
{% set body_id = "login" %}
{% block head_lead %}
<title>{{ _("Snikket Login") }}</title>
@@ -9,16 +9,16 @@
{{ super() }}
{% endblock %}
{% block body %}
<div id="mwrap"><main><div class="form layout-expanded">
<div id="mwrap"><div class="filler"></div><main><div class="form layout-expanded">
<h1 class="form-title">{{ config["SITE_NAME"] }}</h1>
<p class="form-desc">{{ _("Enter your Snikket address and password to manage your account.") }}</p>
<form method="POST" action="{{ url_for('.login') }}" name="login">
<form method="POST" action="{{ url_for('.login') }}" name="login" id="login-form" onsubmit="return domainCheck();" data-addressid="{{ form.address.id }}" data-domain="{{ config["SNIKKET_DOMAIN"] }}">
{{ form.csrf_token }}
{% if form.errors %}
{% call box("alert", _("Login failed")) %}
<p>{{ form.errors.values() | flatten | join(", ")}}</p>
{% endcall %}
{% endif %}
{% call render_errors(form) %}{% endcall %}
<div class="box alert" role="alert" style="display: none;" id="id-warning">
<header>{% trans %}Incorrect address{% endtrans %}</header>
<p>{% trans snikket_domain=config["SNIKKET_DOMAIN"] %}This Snikket service only hosts addresses ending in <em>@{{ snikket_domain }}</em>. Your password was not sent.{% endtrans %}</p>
</div>
<div class="f-ebox">
{{ form.address.label(class="a11y-only") }}
{{ form.address(placeholder=form.address.label.text) }}
@@ -31,8 +31,22 @@
{%- call form_button("login", form.action_signin, class="primary") -%}{% endcall -%}
</div>
</from>
</div></main></div>
<footer>
<ul><li>{% trans about_url=url_for('.about') %}A <a href="{{ about_url }}">Snikket</a> service{% endtrans %}</li></ul>
</footer>
<script type="text/javascript">
var domainCheck = function() {
var form = document.getElementById("login-form");
var addressId = form.dataset.addressid;
var addressField = document.getElementById(addressId);
var domain = form.dataset.domain;
var address = addressField.value;
var errorBox = document.getElementById("id-warning");
if (address.includes("@") && !address.endsWith(domain)) {
errorBox.style.display = "block";
return false;
}
errorBox.style.display = "none";
return true;
};
</script>
</div></main><div class="filler"></div></div>
{%- include "_footer.html" -%}
{% endblock %}

View File

@@ -7,6 +7,25 @@
<div class="filler"></div>
{% block topbar_right %}{% endblock %}
</div>
<div id="mwrap"><main>{% block content %}{% endblock %}</main></div>
<div id="mwrap">
{#- -#}
<div class="flashbox" id="flashbox">
{%- for category, message in get_flashed_messages(True) -%}
<div class="box {{ category }} el-5" role="alert">
{% if category == "success" %}
<header>{% trans %}Operation successful{% endtrans %}</header>
{% elif category == "alert" %}
<header>{% trans %}Error{% endtrans %}</header>
{% endif %}
<p>{{ message }}</p>
</div>
{%- endfor -%}
</div>
{#- -#}
<main>{% block content %}{% endblock %}</main>
{#- -#}
<div class="filler"></div>
{#- -#}
</div>
{%- include "_footer.html" -%}
{% endblock %}

View File

@@ -1,24 +1,48 @@
{% extends "app.html" %}
{% from "library.j2" import clipboard_button, standard_button, avatar with context %}
{% set body_id = "home" %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
{% endblock %}
{% block content %}
<h1>{% trans %}Welcome!{% endtrans %}</h1>
<p>{% trans user_name=user_info.display_name %}Welcome home, {{ user_name }}.{% endtrans %}</p>
<div class="welcome-cards">
<a class="card" href="{{ url_for('.profile') }}">
<h2>{% trans %}Update profile{% endtrans %}</h2>
<p>{% trans %}Change display name, set avatar and configure visibility of your personal data to others.{% endtrans %}</p>
</a>
<a class="card" href="{{ url_for('.change_pw') }}">
<h2>{% trans %}Change password{% endtrans %}</h2>
</a>
{% if user_info.is_admin %}
<a class="card" href="{{ url_for('admin.index') }}">
<h2>{% trans %}Admin dashboard{% endtrans %}</h2>
<p>{% trans %}Manage users and invitations of this Snikket service.{% endtrans %}</p>
</a>
{% endif %}
<a class="card" href="{{ url_for('.logout') }}">
<h2>{% trans %}Log out{% endtrans %}</h2>
<p>{% trans %}Exit the Snikket Web Portal, without logging out your other devices.{% endtrans %}</p>
</a>
</div>
<nav class="welcome">
<ul>
<li class="wide">
<h2>{% trans %}Your account{% endtrans %}</h2>
<div class="profile-card">
<div class="picture" aria-label="Your profile picture">{% call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall %}</div>
<div class="details">
<div class="display-name">{{ user_info.display_name | default(user_info.username) }}</div>
<div class="address">
<input value="{{ user_info.address }}" aria-label="{% trans %}Your XMPP address{% endtrans %}">
{% call clipboard_button(user_info.address) %}{% trans %}Copy address{% endtrans %}{% endcall %}
</div>
</div>
</div>
{#- -#}
{# <p>{% trans %}Change your display name, set a profile picture and control visibility of your personal data to others.{% endtrans %}</p> #}
{#- -#}
{# <img aria-hidden="true" src="{{ url_for("static", filename="img/illus-profile.svg") }}"> #}
{#- -#}
<div>
<div>{% call standard_button("edit", url_for(".profile"), class="primary") %}{% trans %}Edit profile{% endtrans %}{% endcall %}</div>
<div>{% call standard_button("passwd", url_for(".change_pw"), class="secondary") %}{% trans %}Change password{% endtrans %}{% endcall %}</div>
</div>
{#- -#}
</li>
{% if user_info.is_admin %}
<li>
<h2>{% trans %}Your Snikket{% endtrans %}</h2>
{#- -#}
<p>{% trans %}Manage users, invitations and circles of your Snikket service.{% endtrans %}</p>
{#- -#}
<div>{% call standard_button("admin", url_for("admin.index"), class="primary") %}{% trans %}Admin panel{% endtrans %}{% endcall %}</div>
{#- -#}
</li>
{% endif %}
</ul>
</nav>
{% endblock %}

View File

@@ -1,15 +1,12 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, form_button %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
{% block content %}
<div class="form layout-expanded"><form method="POST">
<h2 class="form-title">{% trans %}Sign out of the Snikket Web Portal{% endtrans %}</h2>
<p class="form-desc">{% trans %}Click below to log yourself out of the web portal. This does not affect any other connected devices.{% endtrans %}</p>
{{ form.csrf_token }}
<div class="f-bbox">
{%- call standard_button("back", url_for("user.index"), class="secondary") -%}
{%- call standard_button("back", url_for("user.index"), class="tertiary") -%}
{% trans %}Back{% endtrans %}
{%- endcall -%}
{%- call form_button("logout", form.action_signout, class="primary") %}{% endcall -%}

View File

@@ -1,11 +1,8 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, custom_form_button, render_errors %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
{% block content %}
<div class="form layout-expanded"><form method="POST">
<h2 class="form-title">{% trans %}Change your password{% endtrans %}</h2>
<h1 class="form-title">{% trans %}Change your password{% endtrans %}</h1>
<p class="form-desc weak">{% trans %}To change your password, you need to provide the current password as well as the new one. To reduce the chance of typos, we ask for your new password twice.{% endtrans %}</p>
{{ form.csrf_token }}
{%- call render_errors(form) -%}
@@ -27,8 +24,8 @@
<p>{% trans %}After changing your password, you will have to enter the new password on all of your devices.{% endtrans %}</p>
</div>
<div class="f-bbox">
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call custom_form_button("done", "", "", class="primary") -%}
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call custom_form_button("passwd", "", "", class="primary") -%}
{% trans %}Change password{% endtrans %}
{%- endcall -%}
</div>

View File

@@ -1,29 +1,55 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, form_button %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
{% from "library.j2" import standard_button, form_button, render_errors, avatar with context %}
{% block content %}
<h1>{% trans %}Update your profile{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST" enctype="multipart/form-data">
<h2 class="form-title">{% trans %}Profile{% endtrans %}</h2>
{{ form.csrf_token }}
{% call render_errors(form) %}{% endcall %}
<div class="f-ebox">
{{ form.nickname.label }}
{{ form.nickname(placeholder=user_info.username) }}
</div>
<div class="f-ebox">
{{ form.avatar.label }}
{{ form.avatar }}
<div class="avatar-wrap">
{%- call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall -%}
{{ form.avatar(accept="image/png",
data_maxsize=max_avatar_size,
data_warning_header=avatar_too_big_warning_header,
data_maxsize_warning=avatar_too_big_warning) }}
</div>
</div>
<h3 class="form-title">{% trans %}Visibility{% endtrans %}</h3>
<p class="form-descr weak">{% trans %}This section allows you to control who can see your profile information, like avatar and nickname.{% endtrans %}</p>
<div class="f-ebox">
{{ form.profile_access_model.label }}
{{ form.profile_access_model }}
<fieldset>{#- -#}
<legend>{{ form.profile_access_model.label.text }}</legend>
{{- form.profile_access_model -}}
</fieldset>
</div>
<div class="f-bbox">
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
</div>
<script type="text/javascript">
document.getElementById("{{ form.avatar.id }}").onchange = function() {
var maxsize_s = this.dataset.maxsize;
var maxsize = parseInt(maxsize_s);
var existing_alert = document.getElementById("avatar-alert");
if (existing_alert) {
existing_alert.parentNode.removeChild(existing_alert);
}
if (this.files[0].size > maxsize) {
var warning_header = this.dataset.warningHeader;
var warning_text = this.dataset.maxsizeWarning;
this.setCustomValidity(warning_text);
this.reportValidity();
this.value = null;
} else {
this.setCustomValidity("");
}
};
</script>
</form></div>
{% endblock %}

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -2,21 +2,27 @@ import asyncio
import typing
import quart.flask_patch
from quart import Blueprint, render_template, request, redirect, url_for
from quart import (
Blueprint,
render_template,
request,
redirect,
url_for,
flash,
current_app,
)
import quart.exceptions
import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l, _
from .infra import client
from .infra import client, BaseForm
bp = Blueprint('user', __name__)
class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
class ChangePasswordForm(BaseForm):
current_password = wtforms.PasswordField(
_l("Current password"),
validators=[wtforms.validators.InputRequired()]
@@ -32,12 +38,12 @@ class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
validators=[wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"new_password",
_l("The new passwords must match")
_l("The new passwords must match.")
)]
)
class LogoutForm(flask_wtf.FlaskForm): # type:ignore
class LogoutForm(BaseForm):
action_signout = wtforms.SubmitField(
_l("Sign out"),
)
@@ -50,7 +56,7 @@ _ACCESS_MODEL_CHOICES = [
]
class ProfileForm(flask_wtf.FlaskForm): # type:ignore
class ProfileForm(BaseForm):
nickname = wtforms.TextField(
_l("Display name"),
)
@@ -65,7 +71,7 @@ class ProfileForm(flask_wtf.FlaskForm): # type:ignore
)
action_save = wtforms.SubmitField(
_l("Apply"),
_l("Update profile"),
)
@@ -90,17 +96,29 @@ async def change_pw() -> typing.Union[str, quart.Response]:
quart.exceptions.Forbidden):
# server refused current password, set an appropriate error
form.current_password.errors.append(
_("Incorrect password"),
_("Incorrect password."),
)
else:
await flash(
_("Password changed"),
"success",
)
return redirect(url_for("user.change_pw"))
return await render_template("user_passwd.html", form=form)
EAVATARTOOBIG = _l(
"The chosen avatar is too big. To be able to upload larger "
"avatars, please use the app."
)
@bp.route("/profile", methods=["GET", "POST"])
@client.require_session()
async def profile() -> typing.Union[str, quart.Response]:
max_avatar_size = current_app.config["MAX_AVATAR_SIZE"]
form = ProfileForm()
if request.method != "POST":
user_info = await client.get_user_info()
@@ -114,26 +132,40 @@ async def profile() -> typing.Union[str, quart.Response]:
if form.validate_on_submit():
user_info = await client.get_user_info()
ok = True
file_info = (await request.files).get(form.avatar.name)
if file_info is not None:
mimetype = file_info.mimetype
data = file_info.stream.read()
if len(data) > 0:
if len(data) > max_avatar_size:
print(len(data), max_avatar_size)
form.avatar.errors.append(EAVATARTOOBIG)
ok = False
elif len(data) > 0:
await client.set_user_avatar(data, mimetype)
if user_info.get("nickname") != form.nickname.data:
await client.set_user_nickname(form.nickname.data)
if ok:
if user_info.get("nickname") != form.nickname.data:
await client.set_user_nickname(form.nickname.data)
access_model = form.profile_access_model.data
await asyncio.gather(
client.set_avatar_access_model(access_model),
client.set_vcard_access_model(access_model),
client.set_nickname_access_model(access_model),
)
access_model = form.profile_access_model.data
await asyncio.gather(
client.set_avatar_access_model(access_model),
client.set_vcard_access_model(access_model),
client.set_nickname_access_model(access_model),
)
return redirect(url_for(".profile"))
await flash(
_("Profile updated"),
"success",
)
return redirect(url_for(".profile"))
return await render_template("user_profile.html", form=form)
return await render_template("user_profile.html",
form=form,
max_avatar_size=max_avatar_size,
avatar_too_big_warning_header=_l("Error"),
avatar_too_big_warning=EAVATARTOOBIG)
@bp.route("/logout", methods=["GET", "POST"])
@@ -142,6 +174,12 @@ async def logout() -> typing.Union[quart.Response, str]:
form = LogoutForm()
if form.validate_on_submit():
await client.logout()
# No flashing here because we dont collect flashes in the login page
# and itd be weird.
# await flash(
# _("Logged out"),
# "success",
# )
return redirect(url_for("main.index"))
return await render_template("user_logout.html", form=form)

View File

@@ -1,21 +1,31 @@
action/account_circle:profile
action/bug_report:bug_report
action/done:done
action/delete:delete
action/logout:logout
action/login:login
action/exit_to_app:exit_to_app
action/lock:lock
communication/qr_code:qrcode
communication/vpn_key:passwd
communication/rss_feed:broadcast
content/add_circle_outline:add
content/add_link:create_link
content/create:edit
content/remove_circle_outline:remove
content/content_copy:copy
content/link_off:remove_link
content/send:send
navigation/arrow_back:back
navigation/arrow_forward:forward
navigation/cancel:cancel
navigation/more_vert:more
social/groups:groups
social/people:people
social/group_add:create_group
social/person_add:add_user
social/person_remove:remove_user
navigation/close:close
image/edit:edit
action/admin_panel_settings:admin
content/link:link
content/insights:insights