Compare commits

...

5 Commits

Author SHA1 Message Date
Matthew Wild
2ff47c486a Update translation strings 2024-04-30 10:52:52 +01:00
Matthew Wild
338ee0b278 Add 'share' button for browsers supporting Web Share API 2024-04-30 10:48:51 +01:00
Matthew Wild
64c6548a48 Support for optional text notes on invitations 2024-04-29 18:39:06 +01:00
Matthew Wild
8c824149cc Fixes for invitation display
- Reorder columns, from generic to specific
- Fix empty tooltip on invitation types caused by incorrect macro usage
2024-04-29 18:19:07 +01:00
Matthew Wild
607863cfc4 Remove duplicate template macro 2024-04-29 18:00:22 +01:00
13 changed files with 200 additions and 88 deletions

View File

@@ -301,6 +301,10 @@ class InvitePost(BaseForm):
default="prosody:registered",
)
note = wtforms.StringField(
_l("Comment (optional)"),
)
action_create_invite = wtforms.SubmitField(
_l("New invitation link")
)
@@ -382,12 +386,14 @@ async def create_invite() -> typing.Union[str, werkzeug.Response]:
group_ids=form.circles.data,
role_names=[form.role.data],
ttl=form.lifetime.data,
note=form.note.data,
)
else:
invite = await client.create_account_invite(
group_ids=form.circles.data,
role_names=[form.role.data],
ttl=form.lifetime.data,
note=form.note.data,
)
await flash(
_("Invitation created"),

View File

@@ -162,6 +162,7 @@ class AdminInviteInfo:
group_ids: typing.Collection[str]
role_names: typing.Collection[str]
is_reset: bool
note: typing.Optional[str]
@classmethod
def from_api_response(
@@ -181,6 +182,7 @@ class AdminInviteInfo:
role_names=data.get("roles", []),
reusable=data["reusable"],
is_reset=data.get("reset", False),
note=data.get("note"),
)
@@ -1091,6 +1093,7 @@ class ProsodyClient:
role_names: typing.Collection[str] = [],
restrict_username: typing.Optional[str] = None,
ttl: typing.Optional[int] = None,
note: typing.Optional[str] = None,
session: aiohttp.ClientSession,
) -> AdminInviteInfo:
payload: typing.Dict[str, typing.Any] = {}
@@ -1100,6 +1103,8 @@ class ProsodyClient:
payload["username"] = restrict_username
if ttl is not None:
payload["ttl"] = ttl
if note is not None:
payload["note"] = note
async with session.post(
self._admin_v1_endpoint("/invites/account"),
@@ -1114,6 +1119,7 @@ class ProsodyClient:
group_ids: typing.Collection[str] = [],
role_names: typing.Collection[str] = [],
ttl: typing.Optional[int] = None,
note: typing.Optional[str] = None,
session: aiohttp.ClientSession,
) -> AdminInviteInfo:
payload: typing.Dict[str, typing.Any] = {
@@ -1122,6 +1128,8 @@ class ProsodyClient:
}
if ttl is not None:
payload["ttl"] = ttl
if note is not None:
payload["note"] = note
async with session.post(
self._admin_v1_endpoint("/invites/group"),

View File

@@ -992,19 +992,18 @@ div.profile-card {
}
}
/* clipboard button */
/* clipboard and share buttons */
.copy-to-clipboard {
.copy-to-clipboard, .share-button {
cursor: pointer;
font-style: normal;
text-decoration: none;
}
body.no-copy .copy-to-clipboard {
body.no-copy .copy-to-clipboard, body.no-share .share-button {
display: none !important;
}
/* magic */
pre.guru-meditation {

View File

@@ -193,4 +193,9 @@ licensed under the terms of the Apache 2.0 License -->
<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>
<!-- from: social/share/materialiconsround/24px.svg -->
<symbol id="icon-share" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z" />
</symbol>
</defs></svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -66,6 +66,12 @@
{%- call render_errors(invite_form.circles) -%}{%- endcall -%}
</div>
<!-- Comment -->
<div class="f-ebox">
{{ invite_form.note.label }}
{{ invite_form.note }}
</div>
<div class="f-bbox">
{%- call form_button("create_link", invite_form.action_create_invite, class="primary") %}{% endcall -%}
</div>

View File

@@ -1,5 +1,5 @@
{% extends "admin_app.html" %}
{% from "library.j2" import showuri, form_button, standard_button, extract_circle_name, invite_type_description %}
{% from "library.j2" import showuri, form_button, standard_button, extract_circle_name, invite_type_name, invite_type_description %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
@@ -13,9 +13,10 @@
<dt>{% trans %}Valid until{% endtrans %}</dt>
<dd>{{ invite.expires | format_date }}</dd>
<dt><label for="link-field">{% trans %}Link{% endtrans %}</label></dt>
<dd>{% call showuri(invite.landing_page, id_="link-field") %}{% endcall %}</dd>
<dd>{% call showuri(invite.landing_page, id_="link-field") %}{% trans %}Invitation to Snikket{% endtrans %}{% endcall %}</dd>
<dt>{% trans %}Invitation type{% endtrans %}</dt>
<dd>{% call invite_type_description(invite) %}{% endcall %}</dd>
{% set invite_type = invite.reusable and "group" or "account" %}
<dd><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite_type) %}{% endcall %}">{% call invite_type_name(invite_type) %}{% endcall %}</span></dd>
{%- set ngroups = invite.group_ids | length -%}
{%- if ngroups > 1 -%}
{#- not supported via the web UI, but we should still display it properly -#}

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, invite_type_name, invite_type_description %}
{% from "library.j2" import action_button, icon, clipboard_button, share_button, form_button, custom_form_button, extract_circle_name, invite_type_name, invite_type_description %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
@@ -18,17 +18,18 @@
<col/>
<thead>
<tr>
<th>{% trans %}Expires{% endtrans %}</th>
<th class="collapsible">{% trans %}Type{% endtrans %}</th>
<th class="collapsible">{% trans %}Circle{% endtrans %}</th>
<th>{% trans %}Expires{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for invite in invites %}
{% set invite_type = invite.reusable and "group" or "account" %}
<tr>
<td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</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"><span class="with-tooltip above" data-tooltip="{% call invite_type_description(invite_type) %}{% endcall %}">{% call invite_type_name(invite_type) %}{% endcall %}</span></td>
<td class="collapsible">
{#- -#}
<ul class="inline">
@@ -38,6 +39,8 @@
</ul>
{#- -#}
</td>
<td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</td>
<td>{% if invite.note is not none %}{{ invite.note }}{% endif %}</td>
<td class="nowrap">
{%- call action_button("more", url_for(".edit_invite", id_=invite.id_), class="secondary") -%}
{% trans %}Show invite details{% endtrans %}
@@ -45,6 +48,9 @@
{%- call clipboard_button(invite.landing_page, class="primary") -%}
{% trans %}Copy invite link to clipboard{% endtrans %}
{%- endcall -%}
{%- call share_button("Invitation to Snikket", invite.landing_page, class="primary") -%}
{% trans %}Share invitation link{% endtrans %}
{%- endcall -%}
{%- call custom_form_button("remove_link", form.action_revoke.name, invite.id_, class="secondary danger", slim=True) -%}
{% trans %}Delete invitation{% endtrans %}
{%- endcall -%}

View File

@@ -15,7 +15,7 @@
<dt>{% trans %}Valid until{% endtrans %}</dt>
<dd>{{ reset_link.expires | format_date }}</dd>
<dt><label for="link-field">{% trans %}Link{% endtrans %}</label></dt>
<dd>{% call showuri(reset_link.landing_page, id_="link-field") %}{% endcall %}</dd>
<dd>{% call showuri(reset_link.landing_page, id_="link-field") %}Reset your Snikket password{% endcall %}</dd>
</dd>
<div class="f-bbox">
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}

View File

@@ -16,5 +16,5 @@
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
</head>
<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>
<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 %} no-copy no-share"{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
</html>

View File

@@ -115,8 +115,63 @@ var copy_to_clipboard_btn = function(el) {
});
};
var copy_to_clipboard_btn = function(el) {
var text = el.dataset.cliptext;
if (!text) {
console.error('copy_to_clipboard used on element without text to copy');
}
copyTextToClipboard(text, el, function(success) {
var existing_result_el = document.getElementById("clipboard-result");
if (existing_result_el !== null) {
existing_result_el.parentNode.removeChild(existing_result_el);
}
var icon = "done";
if (!success) {
icon = "cancel";
}
var icon_bak = get_current_icon(el.firstChild);
change_icon(el.firstChild, icon);
setTimeout(function() {
change_icon(el.firstChild, icon_bak);
el.blur();
}, 1500);
});
};
var share_url_btn = function(el) {
let data = {
"title": el.dataset.shareTitle,
"url": el.dataset.shareUrl,
}
let icon_bak = get_current_icon(el.firstChild);
new Promise(function (resolve, reject) {
if(!navigator.canShare || !navigator.canShare(data)) {
return reject();
}
return resolve(navigator.share(data));
}).then(function () {
// Success
change_icon(el.firstChild, "done");
}, function () {
// Failure
change_icon(el.firstChild, "cancel");
}).finally(function () {
// Either way, clear status icon after 1.5s
setTimeout(function() {
change_icon(el.firstChild, icon_bak);
el.blur();
}, 1500);
});
}
window.addEventListener('load', function() {
document.body.classList.remove("no-copy");
if(navigator.share) {
document.body.classList.remove("no-share");
}
});
</script>

View File

@@ -38,7 +38,10 @@
<em>—</em>
{%- else -%}
<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>
<div>
{% call clipboard_button(uri, show_label=True) %}{% trans %}Copy link{% endtrans %}{% endcall %}
{% call share_button(caller() if caller is not none else None, uri, show_label=True) %}{% trans %}Share{% endtrans %}{% endcall %}
</div>
{%- endif -%}
{% endmacro %}
@@ -82,7 +85,7 @@
{% macro clipboard_button(data, show_label=False, caller=None, class=None) -%}
{%- set label = caller() -%}
<a class="button{% if class %} {{ class }}{% endif %}"
<a class="button copy-to-clipboard{% if class %} {{ class }}{% endif %}"
href="#"
{% if not show_label %}
aria-label="{{ label }}"
@@ -97,6 +100,24 @@
</a>
{%- endmacro %}
{% macro share_button(title, url, show_label=False, caller=None, class=None) -%}
{%- set label = caller() -%}
<a class="button share-button{% if class %} {{ class }}{% endif %}"
href="#"
{% if not show_label %}
aria-label="{{ label }}"
title="{{ label }}"
{% endif %}
data-share-title="{{ title }}"
data-share-url="{{ url }}"
onclick="share_url_btn(this); return false;">
{%- call icon("share") %}{% endcall -%}
{%- if show_label %}
<span>{{ label }}</span>
{% endif -%}
</a>
{%- endmacro %}
{% macro render_errors(field, caller=None) -%}
{%- set error_list = field.errors if field.errors is not mapping else (field.errors.values() | flatten | list) -%}
{%- if error_list -%}
@@ -132,19 +153,11 @@
{%- endif -%}
{% endmacro %}
{%- macro invite_type_name(invite_info, caller=None) -%}
{%- if invite_info.reusable -%}
{% trans %}Group{% endtrans %}
{%- else -%}
{%- macro invite_type_name(invite_type, caller=None) -%}
{%- if invite_type == "account" -%}
{% 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 %}
{% trans %}Group{% endtrans %}
{%- endif -%}
{%- endmacro -%}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-04-27 14:22+0200\n"
"POT-Creation-Date: 2024-04-30 10:52+0100\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"
@@ -136,117 +136,121 @@ msgstr ""
msgid "Invitation type"
msgstr ""
#: snikket_web/admin.py:288 snikket_web/templates/library.j2:139
#: snikket_web/admin.py:288 snikket_web/templates/library.j2:158
msgid "Individual"
msgstr ""
#: snikket_web/admin.py:289 snikket_web/templates/library.j2:137
#: snikket_web/admin.py:289 snikket_web/templates/library.j2:160
msgid "Group"
msgstr ""
#: snikket_web/admin.py:305
msgid "Comment (optional)"
msgstr ""
#: snikket_web/admin.py:309
msgid "New invitation link"
msgstr ""
#: snikket_web/admin.py:367
#: snikket_web/admin.py:371
msgid "Revoke"
msgstr ""
#: snikket_web/admin.py:393
#: snikket_web/admin.py:399
msgid "Invitation created"
msgstr ""
#: snikket_web/admin.py:409
#: snikket_web/admin.py:415
msgid "No such invitation exists"
msgstr ""
#: snikket_web/admin.py:424
#: snikket_web/admin.py:430
msgid "Invitation revoked"
msgstr ""
#: snikket_web/admin.py:441 snikket_web/admin.py:489
#: snikket_web/admin.py:447 snikket_web/admin.py:495
#: snikket_web/templates/admin_delete_circle.html:10
#: snikket_web/templates/admin_edit_circle.html:44
msgid "Name"
msgstr ""
#: snikket_web/admin.py:446 snikket_web/templates/admin_circles.html:47
#: snikket_web/admin.py:452 snikket_web/templates/admin_circles.html:47
msgid "Create circle"
msgstr ""
#: snikket_web/admin.py:476
#: snikket_web/admin.py:482
msgid "Circle created"
msgstr ""
#: snikket_web/admin.py:494
#: snikket_web/admin.py:500
msgid "Select user"
msgstr ""
#: snikket_web/admin.py:499
#: snikket_web/admin.py:505
msgid "Update circle"
msgstr ""
#: snikket_web/admin.py:505
#: snikket_web/admin.py:511
msgid "Add user"
msgstr ""
#: snikket_web/admin.py:523 snikket_web/admin.py:622 snikket_web/admin.py:670
#: snikket_web/admin.py:529 snikket_web/admin.py:628 snikket_web/admin.py:676
msgid "No such circle exists"
msgstr ""
#: snikket_web/admin.py:560
#: snikket_web/admin.py:566
msgid "Circle data updated"
msgstr ""
#: snikket_web/admin.py:570
#: snikket_web/admin.py:576
msgid "User added to circle"
msgstr ""
#: snikket_web/admin.py:579
#: snikket_web/admin.py:585
msgid "User removed from circle"
msgstr ""
#: snikket_web/admin.py:588
#: snikket_web/admin.py:594
msgid "Chat removed from circle"
msgstr ""
#: snikket_web/admin.py:606
#: snikket_web/admin.py:612
msgid "Delete circle permanently"
msgstr ""
#: snikket_web/admin.py:633
#: snikket_web/admin.py:639
msgid "Circle deleted"
msgstr ""
#: snikket_web/admin.py:647
#: snikket_web/admin.py:653
msgid "Group chat name"
msgstr ""
#: snikket_web/admin.py:652
#: snikket_web/admin.py:658
msgid "Create group chat"
msgstr ""
#: snikket_web/admin.py:682
#: snikket_web/admin.py:688
msgid "New group chat added to circle"
msgstr ""
#: snikket_web/admin.py:749
#: snikket_web/admin.py:755
msgid "Message contents"
msgstr ""
#: snikket_web/admin.py:755
#: snikket_web/admin.py:761
msgid "Only send to online users"
msgstr ""
#: snikket_web/admin.py:759
#: snikket_web/admin.py:765
msgid "Post to all users"
msgstr ""
#: snikket_web/admin.py:763
#: snikket_web/admin.py:769
msgid "Send preview to yourself"
msgstr ""
#: snikket_web/admin.py:785
#: snikket_web/admin.py:791
msgid "Announcement sent!"
msgstr ""
@@ -546,7 +550,7 @@ msgstr ""
#: snikket_web/templates/admin_circles.html:15
#: snikket_web/templates/admin_edit_circle.html:45
#: snikket_web/templates/admin_edit_circle.html:74
#: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_invites.html:25
#: snikket_web/templates/admin_users.html:10
msgid "Actions"
msgstr ""
@@ -774,7 +778,7 @@ msgid "The user has been deleted from the server."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:84
#: snikket_web/templates/library.j2:131
#: snikket_web/templates/library.j2:152
msgid "deleted"
msgstr ""
@@ -817,38 +821,42 @@ msgstr ""
msgid "Link"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:22
#: snikket_web/templates/admin_edit_invite.html:16
msgid "Invitation to Snikket"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:23
#: snikket_web/templates/admin_home.html:19
msgid "Circles"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:23
#: snikket_web/templates/admin_edit_invite.html:24
msgid "Users joining via this invitation will be added to the following circles:"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:29
#: snikket_web/templates/admin_invites.html:23
#: snikket_web/templates/admin_edit_invite.html:30
#: snikket_web/templates/admin_invites.html:22
msgid "Circle"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:35
#: snikket_web/templates/admin_edit_invite.html:36
msgid "The user will not be added to any circle and will have no contacts."
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:40
#: snikket_web/templates/admin_edit_invite.html:41
msgid "Contact"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:41
#: snikket_web/templates/admin_edit_invite.html:42
#, python-format
msgid "The user will get added as contact of %(peer_jid)s."
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:43
#: snikket_web/templates/admin_edit_invite.html:44
msgid "Created"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:48
#: snikket_web/templates/admin_edit_invite.html:49
msgid "Return to invitation list"
msgstr ""
@@ -1008,26 +1016,34 @@ msgid "Pending invitations"
msgstr ""
#: snikket_web/templates/admin_invites.html:21
msgid "Expires"
msgstr ""
#: snikket_web/templates/admin_invites.html:22
msgid "Type"
msgstr ""
#: snikket_web/templates/admin_invites.html:43
msgid "Show invite details"
#: snikket_web/templates/admin_invites.html:23
msgid "Expires"
msgstr ""
#: snikket_web/templates/admin_invites.html:24
msgid "Comment"
msgstr ""
#: snikket_web/templates/admin_invites.html:46
msgid "Copy invite link to clipboard"
msgid "Show invite details"
msgstr ""
#: snikket_web/templates/admin_invites.html:49
msgid "Copy invite link to clipboard"
msgstr ""
#: snikket_web/templates/admin_invites.html:52
msgid "Share invitation link"
msgstr ""
#: snikket_web/templates/admin_invites.html:55
msgid "Delete invitation"
msgstr ""
#: snikket_web/templates/admin_invites.html:57
#: snikket_web/templates/admin_invites.html:63
msgid "Currently, there are no pending invitations."
msgstr ""
@@ -1565,43 +1581,39 @@ msgstr ""
msgid " (Restricted)"
msgstr ""
#: snikket_web/templates/library.j2:41
#: snikket_web/templates/library.j2:42
msgid "Copy link"
msgstr ""
#: snikket_web/templates/library.j2:104
#: snikket_web/templates/library.j2:43
msgid "Share"
msgstr ""
#: snikket_web/templates/library.j2:125
msgid "Invalid input"
msgstr ""
#: 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:147
msgid "Can be used once to create an account on this Snikket service."
msgstr ""
#: snikket_web/templates/library.j2:153
#: snikket_web/templates/library.j2:166
msgid ""
"Limited users can interact with users on the same Snikket service and be "
"members of circles."
msgstr ""
#: snikket_web/templates/library.j2:155
#: snikket_web/templates/library.j2:168
msgid ""
"Like limited users and can also interact with users on other Snikket "
"services."
msgstr ""
#: snikket_web/templates/library.j2:157
#: snikket_web/templates/library.j2:170
msgid "Like normal users and can access the admin panel in the web portal."
msgstr ""
#: snikket_web/templates/library.j2:171
#: snikket_web/templates/library.j2:184
msgid "Invite a single person (invitation link can only be used once)."
msgstr ""
#: snikket_web/templates/library.j2:173
#: snikket_web/templates/library.j2:186
msgid "Invite a group of people (invitation link can be used multiple times)."
msgstr ""

View File

@@ -36,3 +36,4 @@ image/edit:edit
action/admin_panel_settings:admin
content/link:link
content/insights:insights
social/share:share