From 73f3f25515098694790dffc2bb9b3b659efd44a6 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 10:45:08 +0000 Subject: [PATCH 01/13] Add lock_open and restore_from_trash icons --- snikket_web/static/img/icons.svg | 10 ++++++++++ tools/icons.list | 2 ++ 2 files changed, 12 insertions(+) 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/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 From 6778557db8cc5e0692cba4a8bc8f54111ccdd51f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 10:49:26 +0000 Subject: [PATCH 02/13] On success, return to user listing (edit is complete) --- snikket_web/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snikket_web/admin.py b/snikket_web/admin.py index b6d7cb6..5cbc53d 100644 --- a/snikket_web/admin.py +++ b/snikket_web/admin.py @@ -123,7 +123,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 From e7ed9dd1764f3930e83c68f3fc562a6a0c77514c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 10:50:06 +0000 Subject: [PATCH 03/13] infra: Extend time/date utilities --- snikket_web/infra.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/snikket_web/infra.py b/snikket_web/infra.py index 12ce581..6fb94d3 100644 --- a/snikket_web/infra.py +++ b/snikket_web/infra.py @@ -4,6 +4,8 @@ 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 @@ 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,31 @@ 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() -> datetime: + 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 +114,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) From e5d06877a44103c7b0b35909998d4f41ed6454c1 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 10:50:23 +0000 Subject: [PATCH 04/13] prosodyclient: Update for new mod_http_admin_api (5c589fab6f53) This adds new features including: - User account enabled/disabled status (read and write) - Deletion status (if an account is scheduled for deletion) - Avatar metadata --- snikket_web/prosodyclient.py | 79 +++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/snikket_web/prosodyclient.py b/snikket_web/prosodyclient.py index 80b3a11..04d6103 100644 --- a/snikket_web/prosodyclient.py +++ b/snikket_web/prosodyclient.py @@ -9,7 +9,7 @@ import typing import typing_extensions -from datetime import datetime +from datetime import datetime, timezone import aiohttp @@ -42,6 +42,45 @@ 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.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 +88,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 +118,10 @@ def from_api_response( 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 +972,36 @@ async def update_user( ) 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, From f7c8bccfa2ec01e955c1f55280b91c4556e57715 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 10:52:48 +0000 Subject: [PATCH 05/13] import-icons.sh: Use sensible defaults where possible --- tools/import-icons.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) mode change 100644 => 100755 tools/import-icons.sh 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 '

{% 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 }} From d345f0d98dfc9f9a76594ca20f276b7156b76f3c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 11:11:02 +0000 Subject: [PATCH 07/13] css: Fix dark mode contrast issue for legend text --- snikket_web/scss/app.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/snikket_web/scss/app.scss b/snikket_web/scss/app.scss index a00b521..9f5d45c 100644 --- a/snikket_web/scss/app.scss +++ b/snikket_web/scss/app.scss @@ -1068,6 +1068,10 @@ pre.guru-meditation { } } + label, legend { + color: $gray-800 !important; + } + .box { background-color: black; border-color: $gray-800; From 35e6bec328ec431950b706d1e7461d703fea4920 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 11:11:31 +0000 Subject: [PATCH 08/13] Improvements for admin user listing view --- snikket_web/scss/app.scss | 46 ++++++++++++++++++++++++++ snikket_web/templates/admin_users.html | 22 ++++++------ snikket_web/templates/library.j2 | 23 +++++++++++++ 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/snikket_web/scss/app.scss b/snikket_web/scss/app.scss index 9f5d45c..314c0f3 100644 --- a/snikket_web/scss/app.scss +++ b/snikket_web/scss/app.scss @@ -1206,6 +1206,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 */ @@ -1256,3 +1263,42 @@ 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; + } + + .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/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 @@

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

{% 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 -%} From 1e83881a24d0db06a525b1b3dd82e1124cc6630a Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 11:12:26 +0000 Subject: [PATCH 09/13] Ensure we only have a single primary button to reduce confusion --- snikket_web/templates/admin_edit_user.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snikket_web/templates/admin_edit_user.html b/snikket_web/templates/admin_edit_user.html index 18e30b8..a5ac5e3 100644 --- a/snikket_web/templates/admin_edit_user.html +++ b/snikket_web/templates/admin_edit_user.html @@ -84,14 +84,14 @@

{% trans %}Reset password{% endtrans %}

{% 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 -%}
From 68486911413d61e714fa1021dc7618ad3a35877e Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 11:42:03 +0000 Subject: [PATCH 10/13] css: Remove avatar border and round the edges to match the app --- snikket_web/scss/app.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/snikket_web/scss/app.scss b/snikket_web/scss/app.scss index 314c0f3..5ccbbf8 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; From c63b95c6e083238cd709a9e2e3168a35f8aa71e8 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 11:42:30 +0000 Subject: [PATCH 11/13] Align avatar flush with left edge of container --- snikket_web/scss/app.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/snikket_web/scss/app.scss b/snikket_web/scss/app.scss index 5ccbbf8..063a1ef 100644 --- a/snikket_web/scss/app.scss +++ b/snikket_web/scss/app.scss @@ -1269,6 +1269,10 @@ pre.guru-meditation { .avatar-container { position: relative; + + .avatar { + margin-left: 0; + } } .user-badge-icon { From 46a7d0c37d67d28c4696c8264a4af9c069047307 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 11:51:39 +0000 Subject: [PATCH 12/13] Fix some type annotations --- snikket_web/admin.py | 2 +- snikket_web/infra.py | 22 +++++++++++++++++----- snikket_web/prosodyclient.py | 24 ++++++++++++++++++------ 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/snikket_web/admin.py b/snikket_web/admin.py index 057d7aa..64f8efd 100644 --- a/snikket_web/admin.py +++ b/snikket_web/admin.py @@ -134,7 +134,7 @@ async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]: "success", ) return redirect(url_for(".users")) - except aiohttp.ClientResponseError as exc: + except aiohttp.ClientResponseError: if form.action_restore.data: await flash( _("Could not restore user account"), diff --git a/snikket_web/infra.py b/snikket_web/infra.py index 6fb94d3..4d6469c 100644 --- a/snikket_web/infra.py +++ b/snikket_web/infra.py @@ -87,15 +87,27 @@ def format_last_activity(timestamp: typing.Optional[int]) -> str: yesterday = now - timedelta(days=1) - if last_active.year == now.year and last_active.month == now.month and last_active.day == now.day: + 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: + 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")) + return _.gettext( + "%(time)s ago", + time=flask_babel.format_timedelta(time_ago, granularity="day"), + ) -def template_now() -> datetime: - return dict(now=lambda : datetime.now(timezone.utc)) + +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: diff --git a/snikket_web/prosodyclient.py b/snikket_web/prosodyclient.py index 04d6103..f070279 100644 --- a/snikket_web/prosodyclient.py +++ b/snikket_web/prosodyclient.py @@ -50,15 +50,22 @@ class UserDeletionRequestInfo: @classmethod def from_api_response( cls, - data: typing.Mapping[str, typing.Any], + 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) + 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 @@ -71,7 +78,7 @@ class AvatarMetadata: def from_api_response( cls, data: typing.Mapping[str, typing.Any], - ) -> "AvatarMetadata": + ) -> "AvatarMetadata": return cls( hash=data["hash"], bytes=data["bytes"], @@ -120,8 +127,13 @@ def from_api_response( 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", [])], + 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", []) + ], ) From 55b195cd7fbdfcfd0ad69ddb0eb3bd78369b4e9d Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 8 Dec 2023 12:08:43 +0000 Subject: [PATCH 13/13] Update translations --- snikket_web/translations/messages.pot | 235 ++++++++++++++++++-------- 1 file changed, 162 insertions(+), 73 deletions(-) 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 ""