Compare commits

...

22 Commits

Author SHA1 Message Date
Matthew Wild
7411f4a9e1 prosodyclient: Use empty name if none provided by the server
In parallel, I have updated the server to use the group name for groups with
no name (the MUCs with no name are typically the default auto-created group
MUC).
2023-12-14 12:43:59 +00:00
Matthew Wild
d63ae4768a infra: Fix string to use correct translation function name 2023-12-14 12:43:52 +00:00
Weblate
92a8da724f Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2023-12-12 18:25:32 +00:00
Matthew Wild
ea3a081b6c Merge pull request #167 from snikket-im/fix/remove-useless-qr-js
Remove broken/needless JS from certain pages
2023-12-12 18:25:20 +00:00
Matthew Wild
0647ba2601 Remove broken/needless JS from certain pages 2023-12-12 18:24:01 +00:00
Kim Alvefur
2769036f94 Translated using Weblate (Swedish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/sv/
2023-12-09 16:59:59 +00:00
Weblate
c76befad1c Update translation files
Updated by "Update PO files to match POT (msgmerge)" hook in Weblate.

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/
2023-12-08 12:14:28 +00:00
Matthew Wild
74ecfb8653 Merge pull request #166 from snikket-im/feature/admin-users-ui-updates-dec-23
Admin user management UI updates (Dec 23)
2023-12-08 12:14:19 +00:00
Matthew Wild
55b195cd7f Update translations 2023-12-08 12:08:43 +00:00
Matthew Wild
46a7d0c37d Fix some type annotations 2023-12-08 12:06:43 +00:00
Matthew Wild
c63b95c6e0 Align avatar flush with left edge of container 2023-12-08 11:42:30 +00:00
Matthew Wild
6848691141 css: Remove avatar border and round the edges to match the app 2023-12-08 11:42:03 +00:00
Matthew Wild
1e83881a24 Ensure we only have a single primary button to reduce confusion 2023-12-08 11:12:26 +00:00
Matthew Wild
35e6bec328 Improvements for admin user listing view 2023-12-08 11:11:31 +00:00
Matthew Wild
d345f0d98d css: Fix dark mode contrast issue for legend text 2023-12-08 11:11:02 +00:00
Matthew Wild
f5ccb7d858 admin: Support for unlocking/restoring locked/deleted user accounts 2023-12-08 11:10:32 +00:00
Matthew Wild
f7c8bccfa2 import-icons.sh: Use sensible defaults where possible 2023-12-08 10:52:48 +00:00
Matthew Wild
e5d06877a4 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
2023-12-08 10:50:23 +00:00
Matthew Wild
e7ed9dd176 infra: Extend time/date utilities 2023-12-08 10:50:06 +00:00
Matthew Wild
6778557db8 On success, return to user listing (edit is complete) 2023-12-08 10:49:26 +00:00
Matthew Wild
73f3f25515 Add lock_open and restore_from_trash icons 2023-12-08 10:45:08 +00:00
Matthew Wild
bd66600d05 Merge pull request #165 from snikket-im/feature/multiple-circle-mucs
Support circles with multiple group chats, remove default group chat
2023-11-06 14:26:11 +00:00
27 changed files with 5499 additions and 2206 deletions

View File

@@ -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

View File

@@ -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
@@ -50,7 +53,7 @@ def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable:
def circle_name(c: typing.Any) -> str:
if c.id_ == "default" and c.name == "default":
return _("Main")
return _l("Main")
return c.name
@@ -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)

View File

@@ -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", [])
],
)
@@ -131,7 +190,7 @@ class AdminGroupChatInfo:
return cls(
id_=data["id"],
jid=data["jid"],
name=data["name"],
name=data.get("name", ""),
)
@@ -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,

View File

@@ -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;
}
}

View File

@@ -42,6 +42,16 @@ licensed under the terms of the Apache 2.0 License -->
<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: action/lock_open/materialiconsround/24px.svg -->
<symbol id="icon-lock_open" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M12 13c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6-5h-1V6c0-2.76-2.24-5-5-5-2.28 0-4.27 1.54-4.84 3.75-.14.54.18 1.08.72 1.22.53.14 1.08-.18 1.22-.72C9.44 3.93 10.63 3 12 3c1.65 0 3 1.35 3 3v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 11c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-8c0-.55.45-1 1-1h10c.55 0 1 .45 1 1v8z" />
</symbol>
<!-- from: action/restore_from_trash/materialiconsround/24px.svg -->
<symbol id="icon-restore_from_trash" 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 2v10zm5.65-8.65c.2-.2.51-.2.71 0L16 14h-2v4h-4v-4H8l3.65-3.65zM15.5 4l-.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-1h-2.5z" />
</symbol>
<!-- from: communication/import_export/materialiconsround/24px.svg -->
<symbol id="icon-import_export" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -19,12 +19,33 @@
{% block content %}
<h1>{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}</h1>
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
{% if target_user.deletion_request %}
<div class="box alert">
<header>{% trans %}This user account is pending deletion{% endtrans %}</header>
<p>{% 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 %}
<p>{% 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 %}</p>
<p>{% trans %}If this was a mistake, you can cancel the deletion and restore the account.{% endtrans %}</p>
{%- call form_button("restore_from_trash", form.action_restore, class="secondary") %}{% endcall %}
</div>
{% elif not target_user.enabled %}
<div class="box alert">
<header>{% trans %}This user account is locked{% endtrans %}</header>
<p>{% trans %}The user will not be able to log in to their account until it is unlocked again.{% endtrans %}</p>
{%- call form_button("lock_open", form.action_enable, class="secondary") %}{% endcall %}
</div>
{% endif %}
<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 }}
@@ -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 %}
</p>
<div class="f-bbox">
{%- call form_button("passwd", form.action_create_reset, class="primary") -%}{%- endcall -%}
{%- call form_button("passwd", form.action_create_reset, class="secondary") -%}{%- endcall -%}
</div>
<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") -%}
{%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="secondary") -%}
{%- trans -%}Show debug information{%- endtrans -%}
{%- endcall -%}
</div>

View File

@@ -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 %}
<h1>{% trans %}Manage users{% endtrans %}</h1>
<div class="elevated el-2"><table>
<thead>
<tr>
<th>{% trans %}Login name{% endtrans %}</th>
<th>{% trans %}Display name{% endtrans %}</th>
<th>{% trans %}User{% endtrans %}</th>
<th>{% trans %}Last active{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
@@ -14,15 +14,15 @@
{% for user in users %}
<tr>
<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 -%}
{%- call render_user(user) -%}{%- endcall -%}
</td>
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
{% if user.enabled %}
<td>{{ user.last_active | format_last_activity }}</td>
{% elif user.deletion_request %}
<td>{% trans %}Deleted{% endtrans %}</td>
{% else %}
<td>{% trans %}Locked{% endtrans %}</td>
{% endif %}
<td class="nowrap">
{%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%}
{% trans user_name=user.localpart %}Edit user {{ user_name }}{% endtrans %}

View File

@@ -7,7 +7,6 @@
{% 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">
@@ -27,9 +26,4 @@
{%- 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

@@ -134,7 +134,6 @@
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];

View File

@@ -10,6 +10,29 @@
{%- endif -%}
{%- endmacro %}
{% macro render_user(user, caller=None) -%}
<div class="username-with-avatar">
<div class="avatar-container">
{%- 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 -%}
<div class="user-badge-icon">
<span class="with-tooltip above" data-tooltip="{% trans %}The user is an administrator.{% endtrans %}">{% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %}</span>
</div>
{%- elif user.has_restricted_role -%}
<div class="user-badge-icon">
<span class="with-tooltip above" data-tooltip="{% trans %}The user is restricted.{% endtrans %}">{% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %}</span>
</div>
{%- endif -%}
</div>
<div class="user-info-container">
<div class="user-localpart">{{- user.localpart -}}</div>
{%- if user.display_name %}
<div class="user-display-name">{{- user.display_name -}}</div>
{%- endif %}
</div>
</div>
{%- endmacro -%}
{% macro showuri(uri, caller=None, id_=None) %}
{%- if uri is none -%}
<em>—</em>

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

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

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

View File

@@ -8,28 +8,26 @@ 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-12 18:22+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.13.1\n"
"Generated-By: Babel 2.14.0\n"
#: 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 "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
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
@@ -1187,11 +1260,11 @@ msgstr ""
msgid "Reset your password | Snikket"
msgstr ""
#: snikket_web/templates/invite_reset.html:15
#: snikket_web/templates/invite_reset.html:14
msgid "Reset your password online"
msgstr ""
#: snikket_web/templates/invite_reset.html:16
#: snikket_web/templates/invite_reset.html:15
msgid ""
"To reset your password online, fill out the fields below and confirm "
"using the button."
@@ -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 ""

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

File diff suppressed because it is too large Load Diff

View File

@@ -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

6
tools/import-icons.sh Normal file → Executable file
View File

@@ -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 '<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n<defs>\n' > "$output_file"
printf '<!-- These icons are sourced from Googles Material Icons set,\nlicensed under the terms of the Apache 2.0 License -->\n' >> "$output_file"