diff --git a/snikket_web/admin.py b/snikket_web/admin.py index b6d7cb6..64f8efd 100644 --- a/snikket_web/admin.py +++ b/snikket_web/admin.py @@ -86,6 +86,14 @@ class EditUserForm(BaseForm): _l("Update user"), ) + action_restore = wtforms.SubmitField( + _l("Restore account"), + ) + + action_enable = wtforms.SubmitField( + _l("Unlock account"), + ) + action_create_reset = wtforms.SubmitField( _l("Create password reset link"), ) @@ -112,6 +120,32 @@ async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]: ".user_password_reset_link", id_=reset_link.id_, )) + elif form.action_restore.data or form.action_enable.data: + await client.enable_user_account(localpart) + try: + if form.action_restore.data: + await flash( + _("User account restored"), + "success", + ) + else: + await flash( + _("User account unlocked"), + "success", + ) + return redirect(url_for(".users")) + except aiohttp.ClientResponseError: + if form.action_restore.data: + await flash( + _("Could not restore user account"), + "alert", + ) + else: + await flash( + _("Could not unlock user account"), + "alert", + ) + return redirect(url_for(".edit_user", localpart=localpart)) await client.update_user( localpart, @@ -123,7 +157,7 @@ async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]: _("User information updated."), "success", ) - return redirect(url_for(".edit_user", localpart=localpart)) + return redirect(url_for(".users")) elif request.method == "GET": form.localpart.data = target_user_info.localpart diff --git a/snikket_web/infra.py b/snikket_web/infra.py index 12ce581..4d6469c 100644 --- a/snikket_web/infra.py +++ b/snikket_web/infra.py @@ -4,6 +4,8 @@ import math import secrets import typing +from datetime import datetime, timedelta, timezone + import quart.flask_patch # noqa:F401 from quart import ( current_app, @@ -13,7 +15,8 @@ from quart import ( import flask_babel import flask_wtf -from flask_babel import _ +from flask_babel import lazy_gettext as _l +import flask_babel as _ from . import prosodyclient @@ -70,6 +73,43 @@ def format_bytes(n: float) -> str: return "{} {}".format(n, unit) +def format_last_activity(timestamp: typing.Optional[int]) -> str: + if timestamp is None: + return _l("Never") + + last_active = datetime.fromtimestamp(timestamp, tz=timezone.utc) + # TODO: This 'now' should use the user's local time zone, but we + # don't have that information. Thus 'today'/'yesterday' may be + # slightly inaccurate, but compared to alternative solutions it + # should hopefully be "good enough". + now = datetime.now(tz=timezone.utc) + time_ago = now - last_active + + yesterday = now - timedelta(days=1) + + if ( + last_active.year == now.year + and last_active.month == now.month + and last_active.day == now.day + ): + return _l("Today") + elif ( + last_active.year == yesterday.year + and last_active.month == yesterday.month + and last_active.day == yesterday.day + ): + return _l("Yesterday") + + return _.gettext( + "%(time)s ago", + time=flask_babel.format_timedelta(time_ago, granularity="day"), + ) + + +def template_now() -> typing.Dict[str, typing.Any]: + return dict(now=lambda: datetime.now(timezone.utc)) + + def add_vary_language_header(resp: quart.Response) -> quart.Response: if getattr(g, "language_header_accessed", False): resp.vary.add("Accept-Language") @@ -86,6 +126,8 @@ def init_templating(app: quart.Quart) -> None: app.template_filter("format_bytes")(format_bytes) app.template_filter("flatten")(flatten) app.template_filter("circle_name")(circle_name) + app.template_filter("format_last_activity")(format_last_activity) + app.context_processor(template_now) app.after_request(add_vary_language_header) diff --git a/snikket_web/prosodyclient.py b/snikket_web/prosodyclient.py index 80b3a11..f070279 100644 --- a/snikket_web/prosodyclient.py +++ b/snikket_web/prosodyclient.py @@ -9,7 +9,7 @@ import types import typing import typing_extensions -from datetime import datetime +from datetime import datetime, timezone import aiohttp @@ -42,6 +42,52 @@ class TokenInfo: scopes: typing.Collection[str] +@dataclasses.dataclass(frozen=True) +class UserDeletionRequestInfo: + deleted_at: datetime + pending_until: datetime + + @classmethod + def from_api_response( + cls, + data: typing.Optional[typing.Mapping[str, typing.Any]], + ) -> typing.Optional["UserDeletionRequestInfo"]: + if data is None: + return None + return cls( + deleted_at=datetime.fromtimestamp( + data["deleted_at"], + tz=timezone.utc + ), + pending_until=datetime.fromtimestamp( + data["pending_until"], + tz=timezone.utc + ) + ) + + +@dataclasses.dataclass(frozen=True) +class AvatarMetadata: + bytes: int + hash: str + type: str + width: typing.Optional[int] + height: typing.Optional[int] + + @classmethod + def from_api_response( + cls, + data: typing.Mapping[str, typing.Any], + ) -> "AvatarMetadata": + return cls( + hash=data["hash"], + bytes=data["bytes"], + type=data["type"], + width=data.get("width") or None, + height=data.get("height") or None, + ) + + @dataclasses.dataclass(frozen=True) class AdminUserInfo: localpart: str @@ -49,6 +95,10 @@ class AdminUserInfo: email: typing.Optional[str] phone: typing.Optional[str] roles: typing.Optional[typing.List[str]] + enabled: bool + last_active: typing.Optional[int] + deletion_request: typing.Optional[UserDeletionRequestInfo] + avatar_info: typing.List[AvatarMetadata] @property def has_admin_role(self) -> bool: @@ -75,6 +125,15 @@ class AdminUserInfo: email=data.get("email") or None, phone=data.get("phone") or None, roles=roles, + enabled=data.get("enabled", True), + last_active=data.get("last_active") or None, + deletion_request=UserDeletionRequestInfo.from_api_response( + data.get("deletion_request") + ), + avatar_info=[ + AvatarMetadata.from_api_response(avatar_info) + for avatar_info in data.get("avatar_info", []) + ], ) @@ -925,6 +984,36 @@ class ProsodyClient: ) as resp: self._raise_error_from_response(resp) + @autosession + async def enable_user_account( + self, + localpart: str, + *, + session: aiohttp.ClientSession, + ) -> None: + async with session.patch( + self._admin_v1_endpoint("/users/{}".format(localpart)), + json={ + "enabled": True, + }, + ) as resp: + self._raise_error_from_response(resp) + + @autosession + async def disable_user_account( + self, + localpart: str, + *, + session: aiohttp.ClientSession, + ) -> None: + async with session.patch( + self._admin_v1_endpoint("/users/{}".format(localpart)), + json={ + "enabled": False, + }, + ) as resp: + self._raise_error_from_response(resp) + @autosession async def get_user_debug_info( self, diff --git a/snikket_web/scss/app.scss b/snikket_web/scss/app.scss index a00b521..063a1ef 100644 --- a/snikket_web/scss/app.scss +++ b/snikket_web/scss/app.scss @@ -708,8 +708,7 @@ input[type="submit"], button, .button { height: 1.5em; vertical-align: middle; background-size: cover; - box-shadow: inset 0px 0px 0px 2px rgba(0, 0, 0, 0.2); - border-radius: $w-s4; + border-radius: 10%; margin: 0 0.25em; @@ -1068,6 +1067,10 @@ pre.guru-meditation { } } + label, legend { + color: $gray-800 !important; + } + .box { background-color: black; border-color: $gray-800; @@ -1202,6 +1205,13 @@ pre.guru-meditation { p.form-desc.weak, p.field-desc.weak { color: $gray-700; } + + .user-badge-icon { + color: $gray-900 !important; + background-color: $gray-100 !important; + border-color: $gray-300 !important; + box-shadow: black 0 0 2px !important; + } } /* tooltip magic */ @@ -1252,3 +1262,46 @@ pre.guru-meditation { .with-tooltip:hover:before, .with-tooltip:hover:after { display: block; } + +.username-with-avatar { + display: flex; + align-items: center; + + .avatar-container { + position: relative; + + .avatar { + margin-left: 0; + } + } + + .user-badge-icon { + position: absolute; + bottom: -10px; + right: 0px; + background: white; + border-radius: 50%; + width: 1.2em; + height: 1.2em; + border-color: $gray-500; + border-width: 1px; + border-style: solid; + text-align: center; + margin: 0; + padding: 0; + margin: 0; + padding: 0; + box-shadow: $gray-500 0px 0px 2px; + + line-height: 1; + .icon { + /* vertical-align: text-bottom; */ + padding: 0.1em; + } + } + + .user-info-container { + margin-left: 0.5em; + } + +} diff --git a/snikket_web/static/img/icons.svg b/snikket_web/static/img/icons.svg index 17ecc6d..7b5fbf8 100644 --- a/snikket_web/static/img/icons.svg +++ b/snikket_web/static/img/icons.svg @@ -42,6 +42,16 @@ licensed under the terms of the Apache 2.0 License --> + + + + + + + + + + diff --git a/snikket_web/templates/admin_edit_user.html b/snikket_web/templates/admin_edit_user.html index 7e601a4..a5ac5e3 100644 --- a/snikket_web/templates/admin_edit_user.html +++ b/snikket_web/templates/admin_edit_user.html @@ -19,12 +19,33 @@ {% block content %}

{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}

{{ form.csrf_token }}
+ {% if target_user.deletion_request %} +
+
{% trans %}This user account is pending deletion{% endtrans %}
+

{% trans date=target_user.deletion_request.deleted_at | format_datetime %}The owner of the account sent a deletion request on {{ date }} using their app.{% endtrans %} +

{% trans time=(target_user.deletion_request.pending_until - now())|format_timedelta %}The account has been locked, and will be automatically deleted permanently in {{ time }}.{% endtrans %}

+ +

{% trans %}If this was a mistake, you can cancel the deletion and restore the account.{% endtrans %}

+ + {%- call form_button("restore_from_trash", form.action_restore, class="secondary") %}{% endcall %} +
+ {% elif not target_user.enabled %} +
+
{% trans %}This user account is locked{% endtrans %}
+

{% trans %}The user will not be able to log in to their account until it is unlocked again.{% endtrans %}

+ + {%- call form_button("lock_open", form.action_enable, class="secondary") %}{% endcall %} +
+ {% endif %} +

{% trans %}Edit user{% endtrans %}

+
{{ form.localpart.label }} {{ form.localpart(readonly="readonly") }}

{% trans %}The login name cannot be changed.{% endtrans %}

+
{{ form.display_name.label }} {{ form.display_name }} @@ -63,14 +84,14 @@ {% 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 %}

- {%- call form_button("passwd", form.action_create_reset, class="primary") -%}{%- endcall -%} + {%- call form_button("passwd", form.action_create_reset, class="secondary") -%}{%- endcall -%}

{% trans %}Debug information{% endtrans %}

{% 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 %}

- {%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="primary") -%} + {%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="secondary") -%} {%- trans -%}Show debug information{%- endtrans -%} {%- endcall -%}
diff --git a/snikket_web/templates/admin_users.html b/snikket_web/templates/admin_users.html index 7c1831b..3ad15de 100644 --- a/snikket_web/templates/admin_users.html +++ b/snikket_web/templates/admin_users.html @@ -1,12 +1,12 @@ {% extends "admin_app.html" %} -{% from "library.j2" import action_button, icon, value_or_hint, custom_form_button %} +{% from "library.j2" import action_button, avatar, icon, render_user, value_or_hint, custom_form_button with context %} {% block content %}

{% trans %}Manage users{% endtrans %}

- - + + @@ -14,15 +14,15 @@ {% for user in users %} - + {% if user.enabled %} + + {% elif user.deletion_request %} + + {% else %} + + {% endif %}
{% trans %}Login name{% endtrans %}{% trans %}Display name{% endtrans %}{% trans %}User{% endtrans %}{% trans %}Last active{% endtrans %} {% trans %}Actions{% endtrans %}
- {{- user.localpart -}} - {%- if user.has_admin_role -%} - {% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %} - {%- endif -%} - {%- if user.has_restricted_role -%} - {% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %} - {%- endif -%} + {%- call render_user(user) -%}{%- endcall -%} {% call value_or_hint(user.display_name) %}{% endcall %}{{ user.last_active | format_last_activity }}{% trans %}Deleted{% endtrans %}{% trans %}Locked{% endtrans %} {%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%} {% trans user_name=user.localpart %}Edit user {{ user_name }}{% endtrans %} diff --git a/snikket_web/templates/library.j2 b/snikket_web/templates/library.j2 index d717a23..bb0cef2 100644 --- a/snikket_web/templates/library.j2 +++ b/snikket_web/templates/library.j2 @@ -10,6 +10,29 @@ {%- endif -%} {%- endmacro %} +{% macro render_user(user, caller=None) -%} +
+
+ {%- call avatar(user.localpart+"@"+config["SNIKKET_DOMAIN"], user.avatar_info[0].hash if user.avatar_info | length > 0 else None ) %}{% endcall -%} + {%- if user.has_admin_role -%} +
+ {% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %} +
+ {%- elif user.has_restricted_role -%} +
+ {% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %} +
+ {%- endif -%} +
+ +
+{%- endmacro -%} + {% macro showuri(uri, caller=None, id_=None) %} {%- if uri is none -%} diff --git a/snikket_web/translations/messages.pot b/snikket_web/translations/messages.pot index f73636d..35b3efa 100644 --- a/snikket_web/translations/messages.pot +++ b/snikket_web/translations/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-11-06 13:46+0000\n" +"POT-Creation-Date: 2023-12-08 12:08+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -19,17 +19,15 @@ msgstr "" #: snikket_web/admin.py:69 snikket_web/templates/admin_delete_user.html:10 #: snikket_web/templates/admin_edit_circle.html:73 -#: snikket_web/templates/admin_users.html:8 msgid "Login name" msgstr "" #: snikket_web/admin.py:73 snikket_web/templates/admin_delete_user.html:12 -#: snikket_web/templates/admin_edit_circle.html:74 -#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:63 +#: snikket_web/templates/admin_edit_circle.html:74 snikket_web/user.py:63 msgid "Display name" msgstr "" -#: snikket_web/admin.py:77 snikket_web/templates/admin_edit_user.html:32 +#: snikket_web/admin.py:77 snikket_web/templates/admin_edit_user.html:53 msgid "Access Level" msgstr "" @@ -50,187 +48,228 @@ msgid "Update user" msgstr "" #: snikket_web/admin.py:90 +msgid "Restore account" +msgstr "" + +#: snikket_web/admin.py:94 +msgid "Unlock account" +msgstr "" + +#: snikket_web/admin.py:98 msgid "Create password reset link" msgstr "" -#: snikket_web/admin.py:108 +#: snikket_web/admin.py:116 msgid "Password reset link created" msgstr "" -#: snikket_web/admin.py:123 -msgid "User information updated." +#: snikket_web/admin.py:128 +msgid "User account restored" +msgstr "" + +#: snikket_web/admin.py:133 +msgid "User account unlocked" +msgstr "" + +#: snikket_web/admin.py:140 +msgid "Could not restore user account" msgstr "" #: snikket_web/admin.py:145 +msgid "Could not unlock user account" +msgstr "" + +#: snikket_web/admin.py:157 +msgid "User information updated." +msgstr "" + +#: snikket_web/admin.py:179 msgid "Delete user permanently" msgstr "" -#: snikket_web/admin.py:158 +#: snikket_web/admin.py:192 msgid "User deleted" msgstr "" -#: snikket_web/admin.py:196 +#: snikket_web/admin.py:230 msgid "Password reset link not found" msgstr "" -#: snikket_web/admin.py:208 +#: snikket_web/admin.py:242 msgid "Password reset link deleted" msgstr "" -#: snikket_web/admin.py:228 +#: snikket_web/admin.py:262 msgid "Invite to circle" msgstr "" -#: snikket_web/admin.py:234 +#: snikket_web/admin.py:268 msgid "At least one circle must be selected" msgstr "" -#: snikket_web/admin.py:239 +#: snikket_web/admin.py:273 msgid "Valid for" msgstr "" -#: snikket_web/admin.py:241 +#: snikket_web/admin.py:275 msgid "One hour" msgstr "" -#: snikket_web/admin.py:242 +#: snikket_web/admin.py:276 msgid "Twelve hours" msgstr "" -#: snikket_web/admin.py:243 +#: snikket_web/admin.py:277 msgid "One day" msgstr "" -#: snikket_web/admin.py:244 +#: snikket_web/admin.py:278 msgid "One week" msgstr "" -#: snikket_web/admin.py:245 +#: snikket_web/admin.py:279 msgid "Four weeks" msgstr "" -#: snikket_web/admin.py:251 snikket_web/templates/admin_edit_invite.html:17 +#: snikket_web/admin.py:285 snikket_web/templates/admin_edit_invite.html:17 msgid "Invitation type" msgstr "" -#: snikket_web/admin.py:253 snikket_web/templates/library.j2:116 +#: snikket_web/admin.py:287 snikket_web/templates/library.j2:139 msgid "Individual" msgstr "" -#: snikket_web/admin.py:254 snikket_web/templates/library.j2:114 +#: snikket_web/admin.py:288 snikket_web/templates/library.j2:137 msgid "Group" msgstr "" -#: snikket_web/admin.py:260 +#: snikket_web/admin.py:294 msgid "New invitation link" msgstr "" -#: snikket_web/admin.py:322 +#: snikket_web/admin.py:356 msgid "Revoke" msgstr "" -#: snikket_web/admin.py:346 +#: snikket_web/admin.py:380 msgid "Invitation created" msgstr "" -#: snikket_web/admin.py:362 +#: snikket_web/admin.py:396 msgid "No such invitation exists" msgstr "" -#: snikket_web/admin.py:377 +#: snikket_web/admin.py:411 msgid "Invitation revoked" msgstr "" -#: snikket_web/admin.py:394 snikket_web/admin.py:442 +#: snikket_web/admin.py:428 snikket_web/admin.py:476 #: snikket_web/templates/admin_delete_circle.html:10 #: snikket_web/templates/admin_edit_circle.html:44 msgid "Name" msgstr "" -#: snikket_web/admin.py:399 snikket_web/templates/admin_circles.html:47 +#: snikket_web/admin.py:433 snikket_web/templates/admin_circles.html:47 msgid "Create circle" msgstr "" -#: snikket_web/admin.py:429 +#: snikket_web/admin.py:463 msgid "Circle created" msgstr "" -#: snikket_web/admin.py:447 +#: snikket_web/admin.py:481 msgid "Select user" msgstr "" -#: snikket_web/admin.py:452 +#: snikket_web/admin.py:486 msgid "Update circle" msgstr "" -#: snikket_web/admin.py:458 +#: snikket_web/admin.py:492 msgid "Add user" msgstr "" -#: snikket_web/admin.py:476 snikket_web/admin.py:575 snikket_web/admin.py:623 +#: snikket_web/admin.py:510 snikket_web/admin.py:609 snikket_web/admin.py:657 msgid "No such circle exists" msgstr "" -#: snikket_web/admin.py:513 +#: snikket_web/admin.py:547 msgid "Circle data updated" msgstr "" -#: snikket_web/admin.py:523 +#: snikket_web/admin.py:557 msgid "User added to circle" msgstr "" -#: snikket_web/admin.py:532 +#: snikket_web/admin.py:566 msgid "User removed from circle" msgstr "" -#: snikket_web/admin.py:541 +#: snikket_web/admin.py:575 msgid "Chat removed from circle" msgstr "" -#: snikket_web/admin.py:559 +#: snikket_web/admin.py:593 msgid "Delete circle permanently" msgstr "" -#: snikket_web/admin.py:586 +#: snikket_web/admin.py:620 msgid "Circle deleted" msgstr "" -#: snikket_web/admin.py:600 +#: snikket_web/admin.py:634 msgid "Group chat name" msgstr "" -#: snikket_web/admin.py:605 +#: snikket_web/admin.py:639 msgid "Create group chat" msgstr "" -#: snikket_web/admin.py:635 +#: snikket_web/admin.py:669 msgid "New group chat added to circle" msgstr "" -#: snikket_web/admin.py:702 +#: snikket_web/admin.py:736 msgid "Message contents" msgstr "" -#: snikket_web/admin.py:708 +#: snikket_web/admin.py:742 msgid "Only send to online users" msgstr "" -#: snikket_web/admin.py:712 +#: snikket_web/admin.py:746 msgid "Post to all users" msgstr "" -#: snikket_web/admin.py:716 +#: snikket_web/admin.py:750 msgid "Send preview to yourself" msgstr "" -#: snikket_web/admin.py:738 +#: snikket_web/admin.py:772 msgid "Announcement sent!" msgstr "" -#: snikket_web/infra.py:53 +#: snikket_web/infra.py:56 msgid "Main" msgstr "" +#: snikket_web/infra.py:78 +msgid "Never" +msgstr "" + +#: snikket_web/infra.py:95 +msgid "Today" +msgstr "" + +#: snikket_web/infra.py:101 +msgid "Yesterday" +msgstr "" + +#: snikket_web/infra.py:105 +#, python-format +msgid "%(time)s ago" +msgstr "" + #: snikket_web/invite.py:35 msgid "" "The account data you tried to import is too large to upload. Please " @@ -639,7 +678,7 @@ msgid "Delete user %(user_name)s" msgstr "" #: snikket_web/templates/admin_delete_user.html:6 -#: snikket_web/templates/admin_edit_user.html:53 +#: snikket_web/templates/admin_edit_user.html:74 msgid "Delete user" msgstr "" @@ -708,7 +747,7 @@ msgid "The user has been deleted from the server." msgstr "" #: snikket_web/templates/admin_edit_circle.html:85 -#: snikket_web/templates/library.j2:108 +#: snikket_web/templates/library.j2:131 msgid "deleted" msgstr "" @@ -808,56 +847,90 @@ msgstr "" msgid "Edit user %(user_name)s" msgstr "" -#: snikket_web/templates/admin_edit_user.html:22 -msgid "Edit user" +#: snikket_web/templates/admin_edit_user.html:24 +msgid "This user account is pending deletion" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:25 +#, python-format +msgid "" +"The owner of the account sent a deletion request on %(date)s using their " +"app." msgstr "" #: snikket_web/templates/admin_edit_user.html:26 +#, python-format +msgid "" +"The account has been locked, and will be automatically deleted " +"permanently in %(time)s." +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:28 +msgid "" +"If this was a mistake, you can cancel the deletion and restore the " +"account." +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:34 +msgid "This user account is locked" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:35 +msgid "" +"The user will not be able to log in to their account until it is unlocked" +" again." +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:41 +msgid "Edit user" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:46 msgid "The login name cannot be changed." msgstr "" -#: snikket_web/templates/admin_edit_user.html:33 +#: snikket_web/templates/admin_edit_user.html:54 msgid "" "The access level of a user determines what interactions are allowed for " "them on your Snikket service." msgstr "" -#: snikket_web/templates/admin_edit_user.html:40 +#: snikket_web/templates/admin_edit_user.html:61 #, python-format msgid "%(title)s%(icon)s

%(description)s

" msgstr "" -#: snikket_web/templates/admin_edit_user.html:50 +#: snikket_web/templates/admin_edit_user.html:71 msgid "Return to user list" msgstr "" -#: snikket_web/templates/admin_edit_user.html:58 +#: snikket_web/templates/admin_edit_user.html:79 msgid "Further actions" msgstr "" -#: snikket_web/templates/admin_edit_user.html:60 +#: snikket_web/templates/admin_edit_user.html:81 msgid "Reset password" msgstr "" -#: snikket_web/templates/admin_edit_user.html:63 +#: snikket_web/templates/admin_edit_user.html:84 msgid "" "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." msgstr "" -#: snikket_web/templates/admin_edit_user.html:68 +#: snikket_web/templates/admin_edit_user.html:89 msgid "Debug information" msgstr "" -#: snikket_web/templates/admin_edit_user.html:70 +#: snikket_web/templates/admin_edit_user.html:91 msgid "" "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." msgstr "" -#: snikket_web/templates/admin_edit_user.html:74 +#: snikket_web/templates/admin_edit_user.html:95 msgid "Show debug information" msgstr "" @@ -1048,20 +1121,20 @@ msgid "" "your Snikket server. Use it wisely." msgstr "" -#: snikket_web/templates/admin_users.html:19 -msgid "The user is an administrator." +#: snikket_web/templates/admin_users.html:8 +msgid "User" msgstr "" -#: snikket_web/templates/admin_users.html:19 -msgid " (Administrator)" +#: snikket_web/templates/admin_users.html:9 +msgid "Last active" msgstr "" #: snikket_web/templates/admin_users.html:22 -msgid "The user is restricted." +msgid "Deleted" msgstr "" -#: snikket_web/templates/admin_users.html:22 -msgid " (Restricted)" +#: snikket_web/templates/admin_users.html:24 +msgid "Locked" msgstr "" #: snikket_web/templates/app.html:4 @@ -1455,19 +1528,35 @@ msgstr "" msgid "First install Snikket from F-Droid using the button below:" msgstr "" -#: snikket_web/templates/library.j2:18 +#: snikket_web/templates/library.j2:19 +msgid "The user is an administrator." +msgstr "" + +#: snikket_web/templates/library.j2:19 +msgid " (Administrator)" +msgstr "" + +#: snikket_web/templates/library.j2:23 +msgid "The user is restricted." +msgstr "" + +#: snikket_web/templates/library.j2:23 +msgid " (Restricted)" +msgstr "" + +#: snikket_web/templates/library.j2:41 msgid "Copy link" msgstr "" -#: snikket_web/templates/library.j2:81 +#: snikket_web/templates/library.j2:104 msgid "Invalid input" msgstr "" -#: snikket_web/templates/library.j2:122 +#: snikket_web/templates/library.j2:145 msgid "Can be used multiple times to create accounts on this Snikket service." msgstr "" -#: snikket_web/templates/library.j2:124 +#: snikket_web/templates/library.j2:147 msgid "Can be used once to create an account on this Snikket service." msgstr "" diff --git a/tools/icons.list b/tools/icons.list index 050a9e5..803448e 100644 --- a/tools/icons.list +++ b/tools/icons.list @@ -6,6 +6,8 @@ action/logout:logout action/login:login action/exit_to_app:exit_to_app action/lock:lock +action/lock_open:lock_open +action/restore_from_trash:restore_from_trash communication/import_export:import_export communication/qr_code:qrcode communication/vpn_key:passwd diff --git a/tools/import-icons.sh b/tools/import-icons.sh old mode 100644 new mode 100755 index 92c869b..1603db3 --- a/tools/import-icons.sh +++ b/tools/import-icons.sh @@ -9,9 +9,9 @@ set -euo pipefail # FLAVOR one of '', 'round', 'sharp', 'outlined', 'twoshade' # SVGOUT path to the newly created SVG file root="$1/src" -iconlist_file="$2" -flavor="$3" -output_file="$4" +iconlist_file="${2-tools/icons.list}" +flavor="${3-round}" +output_file="${4-snikket_web/static/img/icons.svg}" printf '