You've already forked snikket-web-portal
Compare commits
41 Commits
feature/pr
...
fix/rc2-ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7411f4a9e1 | ||
|
|
d63ae4768a | ||
|
|
92a8da724f | ||
|
|
ea3a081b6c | ||
|
|
0647ba2601 | ||
|
|
2769036f94 | ||
|
|
c76befad1c | ||
|
|
74ecfb8653 | ||
|
|
55b195cd7f | ||
|
|
46a7d0c37d | ||
|
|
c63b95c6e0 | ||
|
|
6848691141 | ||
|
|
1e83881a24 | ||
|
|
35e6bec328 | ||
|
|
d345f0d98d | ||
|
|
f5ccb7d858 | ||
|
|
f7c8bccfa2 | ||
|
|
e5d06877a4 | ||
|
|
e7ed9dd176 | ||
|
|
6778557db8 | ||
|
|
73f3f25515 | ||
|
|
bd66600d05 | ||
|
|
db363367da | ||
|
|
7ce13b55ac | ||
|
|
d6d4bb5afb | ||
|
|
da52771ebe | ||
|
|
e39b6ca8bb | ||
|
|
14368c5e9a | ||
|
|
2cdcf7f282 | ||
|
|
0f1e76e38c | ||
|
|
ad9af20f12 | ||
|
|
9672cd6870 | ||
|
|
d3a6be7bec | ||
|
|
7a4b56914c | ||
|
|
0f74b1b8f2 | ||
|
|
df78e8a8b0 | ||
|
|
77ccdd5eed | ||
|
|
54b6cad7cd | ||
|
|
fbb618c178 | ||
|
|
bd3d56851b | ||
|
|
c475b83c02 |
28
Dockerfile
28
Dockerfile
@@ -1,28 +1,22 @@
|
||||
FROM debian:bullseye-slim AS build
|
||||
FROM debian:bookworm-slim AS build
|
||||
|
||||
RUN set -eu; \
|
||||
export DEBIAN_FRONTEND=noninteractive ; \
|
||||
apt-get update ; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip python3-setuptools python3-wheel \
|
||||
libpython3-dev \
|
||||
make build-essential;
|
||||
python3 python3-mypy python3-dotenv python3-toml python3-babel python3-distutils \
|
||||
sassc make;
|
||||
|
||||
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
|
||||
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
|
||||
COPY Makefile /opt/snikket-web-portal/Makefile
|
||||
COPY snikket_web/ /opt/snikket-web-portal/snikket_web
|
||||
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
|
||||
|
||||
WORKDIR /opt/snikket-web-portal
|
||||
|
||||
RUN set -eu; \
|
||||
pip3 install -r requirements.txt; \
|
||||
pip3 install -r build-requirements.txt; \
|
||||
make;
|
||||
RUN make
|
||||
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
ARG BUILD_SERIES=dev
|
||||
ARG BUILD_ID=0
|
||||
@@ -33,19 +27,19 @@ ENV SNIKKET_WEB_PYENV=/etc/snikket-web-portal/env.py
|
||||
|
||||
ENV SNIKKET_WEB_PROSODY_ENDPOINT=http://127.0.0.1:5280/
|
||||
|
||||
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
|
||||
|
||||
WORKDIR /opt/snikket-web-portal
|
||||
|
||||
RUN set -eu; \
|
||||
export DEBIAN_FRONTEND=noninteractive ; \
|
||||
apt-get update ; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip python3-setuptools python3-wheel build-essential libpython3-dev netcat; \
|
||||
pip3 install -r requirements.txt; \
|
||||
apt-get remove -y --autoremove build-essential libpython3-dev; \
|
||||
netcat-traditional python3 python3-setuptools python3-pip \
|
||||
python3-aiohttp python3-email-validator python3-flask-babel \
|
||||
python3-flaskext.wtf python3-hsluv python3-hypercorn \
|
||||
python3-quart python3-typing-extensions python3-wtforms ; \
|
||||
pip3 install --break-system-packages environ-config ; \
|
||||
apt-get remove -y --purge python3-pip python3-setuptools; \
|
||||
apt-get clean ; rm -rf /var/lib/apt/lists; \
|
||||
pip3 install hypercorn; \
|
||||
rm -rf /root/.cache;
|
||||
|
||||
HEALTHCHECK CMD nc -zv ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE:-127.0.0.1} ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT:-5765}
|
||||
|
||||
4
Makefile
4
Makefile
@@ -6,7 +6,7 @@ translation_basepath = snikket_web/translations
|
||||
pot_file = $(translation_basepath)/messages.pot
|
||||
|
||||
PYTHON3 ?= python3
|
||||
SCSSC ?= $(PYTHON3) -m scss --load-path snikket_web/scss/
|
||||
SCSSC ?= sassc --load-path snikket_web/scss/
|
||||
|
||||
all: build_css compile_translations
|
||||
|
||||
@@ -14,7 +14,7 @@ build_css: $(generated_css_files)
|
||||
|
||||
$(generated_css_files): snikket_web/static/css/%.css: snikket_web/scss/%.scss $(scss_files) $(scss_includes)
|
||||
mkdir -p snikket_web/static/css/
|
||||
$(SCSSC) -o "$@" "$<"
|
||||
$(SCSSC) "$<" "$@"
|
||||
|
||||
clean:
|
||||
rm -f $(generated_css_files)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pyscss~=1.3
|
||||
mypy
|
||||
python-dotenv~=0.15
|
||||
types-toml
|
||||
|
||||
@@ -5,6 +5,11 @@ if [ -n "${SNIKKET_SITE_NAME:-}" ]; then
|
||||
export SNIKKET_WEB_SITE_NAME="$SNIKKET_SITE_NAME"
|
||||
fi
|
||||
|
||||
export SNIKKET_WEB_TOS_URI="${SNIKKET_TOS_URI}"
|
||||
export SNIKKET_WEB_PRIVACY_URI="${SNIKKET_PRIVACY_URI}"
|
||||
export SNIKKET_WEB_ABUSE_EMAIL="${SNIKKET_ABUSE_EMAIL}"
|
||||
export SNIKKET_WEB_SECURITY_EMAIL="${SNIKKET_SECURITY_EMAIL}"
|
||||
|
||||
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE-127.0.0.1}"
|
||||
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT-5765}"
|
||||
|
||||
|
||||
@@ -170,6 +170,10 @@ class AppConfig:
|
||||
# tools may also very well override it.
|
||||
max_avatar_size = environ.var(1024*1024, converter=int)
|
||||
show_metrics = environ.bool_var(True)
|
||||
tos_uri = environ.var("")
|
||||
privacy_uri = environ.var("")
|
||||
abuse_email = environ.var("")
|
||||
security_email = environ.var("")
|
||||
|
||||
|
||||
_UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1)))
|
||||
@@ -202,6 +206,10 @@ def create_app() -> quart.Quart:
|
||||
app.config["APPLE_STORE_URL"] = config.apple_store_url
|
||||
app.config["MAX_AVATAR_SIZE"] = config.max_avatar_size
|
||||
app.config["SHOW_METRICS"] = config.show_metrics
|
||||
app.config["TOS_URI"] = config.tos_uri
|
||||
app.config["PRIVACY_URI"] = config.privacy_uri
|
||||
app.config["ABUSE_EMAIL"] = config.abuse_email
|
||||
app.config["SECURITY_EMAIL"] = config.security_email
|
||||
|
||||
app.context_processor(proc)
|
||||
app.register_error_handler(
|
||||
|
||||
@@ -77,7 +77,7 @@ class EditUserForm(BaseForm):
|
||||
_l("Access Level"),
|
||||
choices=[
|
||||
("prosody:restricted", _("Limited")),
|
||||
("prosody:user", _l("Normal user")),
|
||||
("prosody:registered", _l("Normal user")),
|
||||
("prosody:admin", _l("Administrator")),
|
||||
],
|
||||
)
|
||||
@@ -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
|
||||
@@ -131,7 +165,7 @@ async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]:
|
||||
if target_user_info.roles:
|
||||
form.role.data = target_user_info.roles[0]
|
||||
else:
|
||||
form.role.data = "prosody:user"
|
||||
form.role.data = "prosody:registered"
|
||||
|
||||
return await render_template(
|
||||
"admin_edit_user.html",
|
||||
@@ -458,6 +492,8 @@ class EditCircleForm(BaseForm):
|
||||
_l("Add user")
|
||||
)
|
||||
|
||||
action_remove_group_chat = wtforms.StringField()
|
||||
|
||||
|
||||
@bp.route("/circle/<id_>", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
@@ -530,6 +566,15 @@ async def edit_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
|
||||
_("User removed from circle"),
|
||||
"success",
|
||||
)
|
||||
elif form.action_remove_group_chat.data:
|
||||
await client.remove_group_chat(
|
||||
id_,
|
||||
form.action_remove_group_chat.data,
|
||||
)
|
||||
await flash(
|
||||
_("Chat removed from circle"),
|
||||
"success",
|
||||
)
|
||||
|
||||
return redirect(url_for(".edit_circle", id_=id_))
|
||||
|
||||
@@ -537,6 +582,7 @@ async def edit_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
|
||||
"admin_edit_circle.html",
|
||||
target_circle=circle,
|
||||
form=form,
|
||||
circle_chats=circle.chats,
|
||||
circle_members=circle_members,
|
||||
invite_form=invite_form,
|
||||
)
|
||||
@@ -583,6 +629,56 @@ async def delete_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
|
||||
)
|
||||
|
||||
|
||||
class AddCircleChatForm(BaseForm):
|
||||
name = wtforms.StringField(
|
||||
_l("Group chat name"),
|
||||
validators=[wtforms.validators.InputRequired()],
|
||||
)
|
||||
|
||||
action_save = wtforms.SubmitField(
|
||||
_l("Create group chat")
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/circle/<id_>/add_chat", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def edit_circle_add_chat(
|
||||
id_: str
|
||||
) -> typing.Union[str, werkzeug.Response]:
|
||||
async with client.authenticated_session() as session:
|
||||
try:
|
||||
circle = await client.get_group_by_id(
|
||||
id_,
|
||||
session=session,
|
||||
)
|
||||
except aiohttp.ClientResponseError as exc:
|
||||
if exc.status == 404:
|
||||
await flash(
|
||||
_("No such circle exists"),
|
||||
"alert",
|
||||
)
|
||||
return redirect(url_for(".circles"))
|
||||
raise
|
||||
|
||||
form = AddCircleChatForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.action_save.data:
|
||||
await client.add_group_chat(id_, form.name.data)
|
||||
await flash(
|
||||
_("New group chat added to circle"),
|
||||
"success",
|
||||
)
|
||||
|
||||
return redirect(url_for(".edit_circle", id_=id_))
|
||||
|
||||
return await render_template(
|
||||
"admin_create_circle_chat.html",
|
||||
target_circle=circle,
|
||||
group_chat_form=form,
|
||||
)
|
||||
|
||||
|
||||
_CPU_EPOCH = time.process_time()
|
||||
_MONOTONIC_EPOCH = time.monotonic()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -173,6 +173,42 @@ async def avatar(from_: str, code: str) -> quart.Response:
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/terms")
|
||||
async def terms() -> Response:
|
||||
if not current_app.config["TOS_URI"]:
|
||||
return Response("", 404)
|
||||
|
||||
return Response("", status=303, headers={
|
||||
"Location": current_app.config["TOS_URI"],
|
||||
})
|
||||
|
||||
|
||||
@bp.route("/privacy")
|
||||
async def privacy() -> Response:
|
||||
if not current_app.config["PRIVACY_URI"]:
|
||||
return Response("", 404)
|
||||
|
||||
return Response("", status=303, headers={
|
||||
"Location": current_app.config["PRIVACY_URI"],
|
||||
})
|
||||
|
||||
|
||||
# This is linked from the iOS app and about page
|
||||
@bp.route("/policies/")
|
||||
async def policies() -> str:
|
||||
return await render_template(
|
||||
"policies.html",
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/.well-known/security.txt")
|
||||
async def securitytxt() -> Response:
|
||||
return Response(
|
||||
await render_template("security.txt"),
|
||||
mimetype="text/plain;charset=UTF-8",
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/_health")
|
||||
async def health() -> Response:
|
||||
return Response("STATUS OK", content_type="text/plain")
|
||||
|
||||
@@ -9,25 +9,27 @@ import types
|
||||
import typing
|
||||
import typing_extensions
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import aiohttp
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from quart import (
|
||||
current_app, _app_ctx_stack, session as http_session, abort, redirect,
|
||||
current_app, session as http_session, abort, redirect,
|
||||
url_for,
|
||||
)
|
||||
import quart
|
||||
|
||||
from flask import g as _app_ctx_stack
|
||||
|
||||
import werkzeug.exceptions
|
||||
|
||||
from . import xmpputil
|
||||
from .xmpputil import split_jid
|
||||
|
||||
|
||||
SCOPE_DEFAULT = "prosody:user"
|
||||
SCOPE_DEFAULT = "prosody:registered"
|
||||
SCOPE_ADMIN = "prosody:admin"
|
||||
|
||||
|
||||
@@ -40,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
|
||||
@@ -47,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:
|
||||
@@ -73,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", [])
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -115,12 +176,30 @@ class AdminInviteInfo:
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class AdminGroupChatInfo:
|
||||
id_: str
|
||||
jid: str
|
||||
name: str
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
cls,
|
||||
data: typing.Mapping[str, typing.Any],
|
||||
) -> "AdminGroupChatInfo":
|
||||
return cls(
|
||||
id_=data["id"],
|
||||
jid=data["jid"],
|
||||
name=data.get("name", ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class AdminGroupInfo:
|
||||
id_: str
|
||||
name: str
|
||||
muc_jid: typing.Optional[str]
|
||||
members: typing.Collection[str]
|
||||
chats: typing.Collection[AdminGroupChatInfo]
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
@@ -130,8 +209,11 @@ class AdminGroupInfo:
|
||||
return cls(
|
||||
id_=data["id"],
|
||||
name=data["name"],
|
||||
muc_jid=data.get("muc_jid") or None,
|
||||
members=data.get("members", []),
|
||||
chats=[
|
||||
AdminGroupChatInfo.from_api_response(x)
|
||||
for x in data.get("chats", [])
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -166,7 +248,7 @@ class HTTPSessionManager:
|
||||
})
|
||||
|
||||
async def teardown(self, exc: typing.Optional[BaseException]) -> None:
|
||||
app_ctx = _app_ctx_stack.top
|
||||
app_ctx = _app_ctx_stack
|
||||
try:
|
||||
session = getattr(app_ctx, self._app_context_attribute)
|
||||
except AttributeError:
|
||||
@@ -183,7 +265,7 @@ class HTTPSessionManager:
|
||||
await session.__aexit__(exc_type, exc, traceback)
|
||||
|
||||
async def __aenter__(self) -> aiohttp.ClientSession:
|
||||
app_ctx = _app_ctx_stack.top
|
||||
app_ctx = _app_ctx_stack
|
||||
try:
|
||||
return getattr(app_ctx, self._app_context_attribute)
|
||||
except AttributeError:
|
||||
@@ -902,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,
|
||||
@@ -1030,7 +1142,7 @@ class ProsodyClient:
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
create_muc: bool = True,
|
||||
create_muc: bool = False,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> AdminGroupInfo:
|
||||
payload = {
|
||||
@@ -1105,6 +1217,27 @@ class ProsodyClient:
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def add_group_chat(
|
||||
self,
|
||||
id_: str,
|
||||
name: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
|
||||
payload: typing.Dict[str, typing.Any] = {
|
||||
"name": name,
|
||||
}
|
||||
|
||||
async with session.post(
|
||||
self._admin_v1_endpoint(
|
||||
"/groups/{}/chats".format(id_)
|
||||
),
|
||||
json=payload,
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def remove_group_member(
|
||||
self,
|
||||
@@ -1120,6 +1253,21 @@ class ProsodyClient:
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def remove_group_chat(
|
||||
self,
|
||||
group_id: str,
|
||||
chat_id: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
async with session.delete(
|
||||
self._admin_v1_endpoint(
|
||||
"/groups/{}/chats/{}".format(group_id, chat_id)
|
||||
),
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def delete_group(
|
||||
self,
|
||||
@@ -1160,7 +1308,6 @@ class ProsodyClient:
|
||||
self._raise_error_from_response(resp)
|
||||
return True
|
||||
|
||||
@autosession
|
||||
async def revoke_token(
|
||||
self,
|
||||
*,
|
||||
@@ -1174,7 +1321,8 @@ class ProsodyClient:
|
||||
|
||||
async def logout(self) -> None:
|
||||
try:
|
||||
await self.revoke_token()
|
||||
async with self._plain_session as session:
|
||||
await self.revoke_token(session=session)
|
||||
except aiohttp.ClientError:
|
||||
self.logger.warn("failed to revoke token!",
|
||||
exc_info=True)
|
||||
|
||||
@@ -275,22 +275,22 @@ div.form.layout-expanded {
|
||||
}
|
||||
|
||||
@each $type in $text-entry-inputs {
|
||||
input[type=$type] {
|
||||
input[type=#{$type}] {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-bottom: $w-s4 solid $primary-500;
|
||||
margin-bottom: -$w-s4;
|
||||
}
|
||||
|
||||
input[type=$type].has-error {
|
||||
input[type=#{$type}].has-error {
|
||||
border-right: $w-s4 solid $alert-500;
|
||||
}
|
||||
|
||||
input[type=$type]:hover {
|
||||
input[type=#{$type}]:hover {
|
||||
border-bottom-color: $primary-700;
|
||||
}
|
||||
|
||||
input[type=$type]:focus {
|
||||
input[type=#{$type}]:focus {
|
||||
border-bottom-color: $primary-800;
|
||||
}
|
||||
}
|
||||
@@ -646,69 +646,6 @@ input[type="submit"], button, .button {
|
||||
|
||||
|
||||
|
||||
/* button, .button {
|
||||
margin: 0 $w-s2;
|
||||
}
|
||||
|
||||
button.lv-primary, .button.lv-primary {
|
||||
background-color: $gray-500;
|
||||
color: $gray-900;
|
||||
border-radius: $w-s4;
|
||||
border: $w-s4 solid $gray-400;
|
||||
|
||||
@each $type, $values in $colours {
|
||||
&.c-#{$type} {
|
||||
border-color: nth($values, 4);
|
||||
background-color: nth($values, 5);
|
||||
color: nth($values, 9);
|
||||
}
|
||||
|
||||
&.c-#{$type}:hover {
|
||||
background-color: nth($values, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.lv-secondary, .button.lv-secondary {
|
||||
background-color: $gray-700;
|
||||
color: $gray-100;
|
||||
border-radius: $w-s4;
|
||||
|
||||
@each $type, $values in $colours {
|
||||
&.c-#{$type} {
|
||||
background-color: nth($values, 7);
|
||||
color: nth($values, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.lv-tertiary, .button.lv-tertiary {
|
||||
background-color: inherit;
|
||||
color: $gray-300;
|
||||
border-radius: $w-s4;
|
||||
text-decoration: underline;
|
||||
|
||||
@each $type, $values in $colours {
|
||||
&.c-#{$type} {
|
||||
color: nth($values, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
button.lv-secondary.c-#{$type}, .button.lv-secondary.c-#{$type} {
|
||||
background-color: nth($values, 7);
|
||||
color: nth($values, 1);
|
||||
}
|
||||
|
||||
button.lv-tertiary.c-#{$type}, .button.lv-tertiary.c-#{$type} {
|
||||
color: nth($values, 3);
|
||||
text-decoration: underline;
|
||||
background-color: transparent;
|
||||
}
|
||||
}*/
|
||||
|
||||
/* boxes */
|
||||
|
||||
.box {
|
||||
@@ -771,8 +708,7 @@ button.lv-tertiary, .button.lv-tertiary {
|
||||
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;
|
||||
|
||||
@@ -1121,7 +1057,7 @@ pre.guru-meditation {
|
||||
}
|
||||
|
||||
@each $type in $text-entry-inputs {
|
||||
input[type=$type] {
|
||||
input[type=#{$type}] {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
@@ -1131,6 +1067,10 @@ pre.guru-meditation {
|
||||
}
|
||||
}
|
||||
|
||||
label, legend {
|
||||
color: $gray-800 !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: black;
|
||||
border-color: $gray-800;
|
||||
@@ -1265,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 */
|
||||
@@ -1315,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -6,16 +6,20 @@
|
||||
{% block body %}
|
||||
<main>
|
||||
<div class="box el-2">
|
||||
<h1>{% trans %}About Snikket{% endtrans %}</h1>
|
||||
<p>{% trans snikket_url="https://snikket.org" %}To learn more about Snikket, visit the <a href="{{ snikket_url}}">Snikket website</a>.{% endtrans %}</p>
|
||||
<h2>{% trans %}About this Service{% endtrans %}</h2>
|
||||
<p>{% trans site_name=config["SITE_NAME"] %}This is the Snikket service <em>{{ site_name }}</em>.{% endtrans %}</p>
|
||||
<p>{% trans site_name=config["SITE_NAME"] %}This is the Snikket service <em>{{ site_name }}</em>, running open-source software from the Snikket project.{% endtrans %}</p>
|
||||
<p>{% trans snikket_url="https://snikket.org" %}To learn more about Snikket, visit the <a href="{{ snikket_url}}">Snikket website</a>.{% endtrans %}</p>
|
||||
|
||||
<p><a href="/policies/">{% trans %}View service policies{% endtrans %}</a>
|
||||
|
||||
<h3>{% trans %}Licenses{% endtrans %}</h3>
|
||||
<p>{% trans agpl_url="https://www.gnu.org/licenses/agpl.html" %}The web portal software is licensed under the terms of the <a href="{{ agpl_url }}">Affero GNU General Public License, version 3.0 or later</a>. The full terms of the license can be reviewed using the aforementioned link.{% endtrans %}</p>
|
||||
<p>{% trans source_url="https://github.com/snikket-im/snikket-web-portal/" %}The source code of the web portal can be downloaded and viewed in <a href="{{ source_url }}">its GitHub repository</a>.{% endtrans %}</p>
|
||||
<p>{% trans source_url="https://material.io/resources/icons/", apache20_url="https://www.apache.org/licenses/LICENSE-2.0.txt" %}The icons used in the web portal are <a href="{{ source_url }}">Google’s Material Icons</a>, made available by Google under the terms of the <a href="{{ apache20_url }}">Apache 2.0 License</a>.{% endtrans %}</p>
|
||||
|
||||
<h3>{% trans %}Trademarks{% endtrans %}</h3>
|
||||
<p>{% trans trademarks_url="https://snikket.org/about/trademarks/" %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company. For more information about the trademarks, visit the <a href="{{ trademarks_url }}">Snikket Trademarks information page</a>.{% endtrans %}
|
||||
|
||||
<h3>{% trans %}Software Versions{% endtrans %}</h3>
|
||||
<pre>Domain: {{ config["SNIKKET_DOMAIN"] }}
|
||||
Web Portal{% if version %} ({{ version }}){% endif %}
|
||||
@@ -27,6 +31,7 @@ Web Portal{% if version %} ({{ version }}){% endif %}
|
||||
{% for name, version in extra_versions.items() %}
|
||||
{{ name }} ({{ version }}){% endfor %}
|
||||
{%- endif -%}</pre>
|
||||
|
||||
<p>
|
||||
{%- call standard_button("back", url_for("index"), class="primary") -%}
|
||||
{% trans %}Back to the main page{% endtrans %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
<h1>{% trans %}Manage circles{% endtrans %}</h1>
|
||||
<p>{% trans %}<em>Circles</em> aim to help people who are in the same social circle find each other on your service.{% endtrans %}</p>
|
||||
<p>{% trans %}Users who are in the same circle will see each other in their contact list. In addition, each circle has a group chat where the circle members are included.{% endtrans %}</p>
|
||||
<p>{% trans %}Users who are in the same circle will see each other in their contact list. In addition, each circle may have group chats where the circle members are included.{% endtrans %}</p>
|
||||
{%- if circles -%}
|
||||
<form method="POST" action="{{ url_for(".create_invite") }}">
|
||||
{{- invite_form.csrf_token -}}
|
||||
|
||||
5
snikket_web/templates/admin_create_circle_chat.html
Normal file
5
snikket_web/templates/admin_create_circle_chat.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% block content %}
|
||||
<h1>{{ target_circle.name }}</h1>
|
||||
{%- include "admin_create_circle_group_chat_form.html" -%}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% from "library.j2" import form_button, render_errors %}
|
||||
<form method="POST" action="{{ url_for(".edit_circle_add_chat", id_=target_circle.id_) }}">
|
||||
{{- group_chat_form.csrf_token -}}
|
||||
<div class="form layout-expanded">
|
||||
<h2 class="form-title">{% trans %}Create new circle group chat{% endtrans %}</h2>
|
||||
<p class="form-descr weak">{% trans %}Add a chat to your circle so its members can hold group discussions.{% endtrans %}</p>
|
||||
<p class="form-descr weak"><strong>{% trans %}Tip:{% endtrans %}</strong> {% trans %}This is only for creating group chats that automatically include <em>all</em> members of the circle. If you want a normal group chat, create it in the Snikket app instead.{% endtrans %}</p>
|
||||
<div class="f-ebox">
|
||||
{{ group_chat_form.name.label }}
|
||||
{{ group_chat_form.name }}
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("add", group_chat_form.action_save, class="primary") %}{% endcall -%}
|
||||
</div>
|
||||
</div></form>
|
||||
@@ -13,13 +13,6 @@
|
||||
<div class="box hint form layout-expanded">
|
||||
<header>{% trans %}This is your main circle{% endtrans %}</header>
|
||||
<p>{% trans %}This circle is managed automatically and cannot be removed or renamed.{% endtrans %}</p>
|
||||
{%- if target_circle.muc_jid -%}
|
||||
<div><label for="circle-muc-jid">{% trans %}Group chat address{% endtrans %}</label></div>
|
||||
<div><input type="text" readonly="readonly" id="circle-muc-jid" value="{{ target_circle.muc_jid }}"></div>
|
||||
{%- call clipboard_button(target_circle.muc_jid, show_label=True) -%}
|
||||
{%- trans -%}Copy address{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- else -%}
|
||||
<div class="form layout-expanded">
|
||||
@@ -28,17 +21,6 @@
|
||||
{{ form.name.label }}
|
||||
{{ form.name }}
|
||||
</div>
|
||||
<div class="f-ebox">
|
||||
{%- if target_circle.muc_jid -%}
|
||||
<label for="circle-muc-jid">{% trans %}Group chat address{% endtrans %}</label>
|
||||
<input type="text" readonly="readonly" id="circle-muc-jid" value="{{ target_circle.muc_jid }}">
|
||||
{%- call clipboard_button(target_circle.muc_jid, show_label=True) -%}
|
||||
{%- trans -%}Copy address{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
{%- else -%}
|
||||
<p>{% trans %}This circle has no group chat associated.{% endtrans %}<p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for(".circles"), class="tertiary") -%}
|
||||
{% trans %}Return to circle list{% endtrans %}
|
||||
@@ -52,7 +34,39 @@
|
||||
</div>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
|
||||
<h2 id="chats">{% trans %}Group chats{% endtrans %}</h2>
|
||||
<p>{% trans %}These group chats will be available to all members of the circle.{% endtrans %}</p>
|
||||
|
||||
{%- if circle_chats -%}
|
||||
<div class="el-2 elevated"><table>
|
||||
<thead>
|
||||
<th>{% trans %}Name{% endtrans %}</th>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for chat in circle_chats -%}
|
||||
<tr>
|
||||
<td class="collapsible">{% call value_or_hint(chat.name) %}{% endcall %}</td>
|
||||
<td class="nowrap">
|
||||
{%- call custom_form_button("delete", form.action_remove_group_chat.name, chat.id_, class="primary danger", slim=True) -%}
|
||||
{% trans name=chat.name %}Delete group chat '{{ name }}'{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{%- else -%}
|
||||
<p>{% trans %}This circle currently has no group chats.{% endtrans %}</p>
|
||||
{%- endif -%}
|
||||
{%- call standard_button("add", url_for(".edit_circle_add_chat", id_=target_circle.id_), class="secondary") -%}
|
||||
{% trans %}Add group chat{% endtrans %}
|
||||
{%- endcall -%}
|
||||
|
||||
<h2 id="members">{% trans %}Circle members{% endtrans %}</h2>
|
||||
<p>{% trans %}All members of the circle will see each other in their contact list.{% endtrans %}</p>
|
||||
|
||||
{%- if circle_members -%}
|
||||
<div class="el-2 elevated"><table>
|
||||
<thead>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% macro access_level_description(role, caller=None) %}
|
||||
{%- if role == "prosody:restricted" -%}
|
||||
{% trans %}Limited users can interact with users on the same Snikket service and be members of circles.{% endtrans %}
|
||||
{%- elif role == "prosody:user" -%}
|
||||
{%- elif role == "prosody:registered" -%}
|
||||
{% trans %}Like limited users and can also interact with users on other Snikket services.{% endtrans %}
|
||||
{%- elif role == "prosody:admin" -%}
|
||||
{% trans %}Like normal users and can access the admin panel in the web portal.{% endtrans %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
{%- else -%}
|
||||
<p>{% trans site_name=config["SITE_NAME"] %}You have been invited to chat on {{ site_name }} using Snikket, a secure, privacy-friendly chat app.{% endtrans %}</p>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if config["TOS_URI"] and config["PRIVACY_URI"] -%}
|
||||
<p>
|
||||
{% trans site_name=config["SITE_NAME"], tos_uri=config["TOS_URI"], privacy_uri=config["PRIVACY_URI"] %}By continuing, you agree to the <a href="{{tos_uri}}">Terms of Service</a> and <a href="{{privacy_uri}}">Privacy Policy</a>.{% endtrans %}
|
||||
</p>
|
||||
{%- endif -%}
|
||||
|
||||
<h2>{% trans %}Get started{% endtrans %}</h2>
|
||||
{%- if apple_store_url -%}
|
||||
<p>{% trans %}Install the Snikket App on your Android or iOS device.{% endtrans %}</p>
|
||||
@@ -127,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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
39
snikket_web/templates/policies.html
Normal file
39
snikket_web/templates/policies.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "library.j2" import standard_button %}
|
||||
{% block head_lead %}
|
||||
<title>{% trans %}Policies{% endtrans %} - {{ config["SITE_NAME"] }}</title>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<main>
|
||||
<div class="box el-2">
|
||||
<h1>{{ config["SITE_NAME"] }}</h1>
|
||||
<h2>{% trans %}Policies{% endtrans %}</h2>
|
||||
|
||||
{% if config["TOS_URI"] or config["PRIVACY_URI"] -%}
|
||||
<p>{% trans %}Use of this service is subject to the following policies:{% endtrans %}</p>
|
||||
<ul>
|
||||
{%- if config["TOS_URI"] %}
|
||||
<li><a href="{{ config["TOS_URI"] }}">{% trans %}Terms of Service{% endtrans %}</a></li>
|
||||
{%- endif %}
|
||||
{%- if config["PRIVACY_URI"] %}
|
||||
<li><a href="{{ config["PRIVACY_URI"] }}">{% trans %}Privacy Policy{% endtrans %}</a></li>
|
||||
{%- endif %}
|
||||
</ul>
|
||||
{%- else -%}
|
||||
<p>{% trans %}Please contact the administrator of this instance if you have questions about policies.{% endtrans %}</p>
|
||||
{% endif -%}
|
||||
|
||||
<p>{% trans url="https://snikket.org/app/privacy/" %}Use of the Snikket apps is subject to the <a href="{{url}}">Snikket Apps Privacy Policy</a>.{% endtrans %}</p>
|
||||
|
||||
{%- if config["ABUSE_EMAIL"] %}
|
||||
<p>{% trans email=config["ABUSE_EMAIL"], domain=config["SNIKKET_DOMAIN"] %}To report policy violations or other abuse from this service, please send an email to {{email}}. Specify the domain name of this instance ({{domain}}) and include details of the incident(s).{% endtrans %}</p>
|
||||
{%- endif %}
|
||||
|
||||
<p>
|
||||
{%- call standard_button("back", url_for("index"), class="primary") -%}
|
||||
{% trans %}Back to the main page{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
16
snikket_web/templates/security.txt
Normal file
16
snikket_web/templates/security.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
# {{ config["SNIKKET_DOMAIN"] }} is running open-source software
|
||||
# from the Snikket project: https://snikket.org/
|
||||
|
||||
{% if config["SECURITY_EMAIL"] -%}
|
||||
# Security issues related to this service should be addressed to the
|
||||
# following security contact:
|
||||
Contact: mailto:{{ config["SECURITY_EMAIL"] }}
|
||||
{% else -%}
|
||||
# This service does not have a public security contact. You might find
|
||||
# more information about the service at the following link:
|
||||
Contact: https://{{ config["SNIKKET_DOMAIN"] }}/policies/
|
||||
{%- endif %}
|
||||
|
||||
# Please report software defects to the project developers, per the
|
||||
# instructions at the following link:
|
||||
Contact: https://snikket.org/security/
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,28 +8,26 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2023-04-01 10:07+0200\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.12.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:59
|
||||
#: snikket_web/templates/admin_users.html:8
|
||||
#: snikket_web/templates/admin_edit_circle.html:73
|
||||
msgid "Login name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:73 snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_edit_circle.html:60
|
||||
#: 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,170 +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:474 snikket_web/admin.py:563
|
||||
#: 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:511
|
||||
#: snikket_web/admin.py:547
|
||||
msgid "Circle data updated"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:521
|
||||
#: snikket_web/admin.py:557
|
||||
msgid "User added to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:530
|
||||
#: snikket_web/admin.py:566
|
||||
msgid "User removed from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:547
|
||||
#: snikket_web/admin.py:575
|
||||
msgid "Chat removed from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:593
|
||||
msgid "Delete circle permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:574
|
||||
#: snikket_web/admin.py:620
|
||||
msgid "Circle deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:640
|
||||
#: snikket_web/admin.py:634
|
||||
msgid "Group chat name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:639
|
||||
msgid "Create group chat"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:669
|
||||
msgid "New group chat added to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:736
|
||||
msgid "Message contents"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:646
|
||||
#: snikket_web/admin.py:742
|
||||
msgid "Only send to online users"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:650
|
||||
#: snikket_web/admin.py:746
|
||||
msgid "Post to all users"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:654
|
||||
#: snikket_web/admin.py:750
|
||||
msgid "Send preview to yourself"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:676
|
||||
#: 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 "
|
||||
@@ -382,31 +438,37 @@ msgid ""
|
||||
"Interest Company."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:4 snikket_web/templates/about.html:9
|
||||
#: snikket_web/templates/about.html:4
|
||||
msgid "About Snikket"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:9
|
||||
msgid "About this Service"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:10
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This is the Snikket service <em>%(site_name)s</em>, running open-source "
|
||||
"software from the Snikket project."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:11
|
||||
#, python-format
|
||||
msgid ""
|
||||
"To learn more about Snikket, visit the <a "
|
||||
"href=\"%(snikket_url)s\">Snikket website</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:11
|
||||
msgid "About this Service"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:12
|
||||
#, python-format
|
||||
msgid "This is the Snikket service <em>%(site_name)s</em>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:13
|
||||
msgid "View service policies"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:15
|
||||
msgid "Licenses"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:14
|
||||
#: snikket_web/templates/about.html:16
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The web portal software is licensed under the terms of the <a "
|
||||
@@ -415,14 +477,14 @@ msgid ""
|
||||
"aforementioned link."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:15
|
||||
#: snikket_web/templates/about.html:17
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The source code of the web portal can be downloaded and viewed in <a "
|
||||
"href=\"%(source_url)s\">its GitHub repository</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:16
|
||||
#: snikket_web/templates/about.html:18
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The icons used in the web portal are <a href=\"%(source_url)s\">Google’s "
|
||||
@@ -430,11 +492,11 @@ msgid ""
|
||||
"href=\"%(apache20_url)s\">Apache 2.0 License</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:17
|
||||
#: snikket_web/templates/about.html:20
|
||||
msgid "Trademarks"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:18
|
||||
#: snikket_web/templates/about.html:21
|
||||
#, python-format
|
||||
msgid ""
|
||||
"“Snikket” and the parrot logo are trademarks of Snikket Community "
|
||||
@@ -442,11 +504,11 @@ msgid ""
|
||||
" href=\"%(trademarks_url)s\">Snikket Trademarks information page</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:19
|
||||
#: snikket_web/templates/about.html:23
|
||||
msgid "Software Versions"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:32
|
||||
#: snikket_web/templates/about.html:37 snikket_web/templates/policies.html:34
|
||||
msgid "Back to the main page"
|
||||
msgstr ""
|
||||
|
||||
@@ -468,8 +530,8 @@ msgstr ""
|
||||
#: snikket_web/templates/admin_circles.html:6
|
||||
msgid ""
|
||||
"Users who are in the same circle will see each other in their contact "
|
||||
"list. In addition, each circle has a group chat where the circle members "
|
||||
"are included."
|
||||
"list. In addition, each circle may have group chats where the circle "
|
||||
"members are included."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_circles.html:13
|
||||
@@ -481,7 +543,8 @@ msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_circles.html:15
|
||||
#: snikket_web/templates/admin_edit_circle.html:61
|
||||
#: snikket_web/templates/admin_edit_circle.html:45
|
||||
#: snikket_web/templates/admin_edit_circle.html:75
|
||||
#: snikket_web/templates/admin_invites.html:24
|
||||
#: snikket_web/templates/admin_users.html:10
|
||||
msgid "Actions"
|
||||
@@ -517,6 +580,25 @@ msgstr ""
|
||||
msgid "New circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_create_circle_group_chat_form.html:5
|
||||
msgid "Create new circle group chat"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_create_circle_group_chat_form.html:6
|
||||
msgid "Add a chat to your circle so its members can hold group discussions."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_create_circle_group_chat_form.html:7
|
||||
msgid "Tip:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_create_circle_group_chat_form.html:7
|
||||
msgid ""
|
||||
"This is only for creating group chats that automatically include "
|
||||
"<em>all</em> members of the circle. If you want a normal group chat, "
|
||||
"create it in the Snikket app instead."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_create_invite.html:3
|
||||
msgid "Create invitation"
|
||||
msgstr ""
|
||||
@@ -559,8 +641,8 @@ msgid "Delete circle %(circle_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_circle.html:6
|
||||
#: snikket_web/templates/admin_edit_circle.html:48
|
||||
#: snikket_web/templates/admin_edit_circle.html:51
|
||||
#: snikket_web/templates/admin_edit_circle.html:30
|
||||
#: snikket_web/templates/admin_edit_circle.html:33
|
||||
msgid "Delete circle"
|
||||
msgstr ""
|
||||
|
||||
@@ -596,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 ""
|
||||
|
||||
@@ -619,69 +701,78 @@ msgstr ""
|
||||
msgid "This circle is managed automatically and cannot be removed or renamed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:17
|
||||
#: snikket_web/templates/admin_edit_circle.html:33
|
||||
msgid "Group chat address"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:20
|
||||
#: snikket_web/templates/admin_edit_circle.html:36
|
||||
#: snikket_web/templates/invite_success.html:15
|
||||
#: snikket_web/templates/user_home.html:21
|
||||
msgid "Copy address"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:26
|
||||
#: snikket_web/templates/admin_edit_circle.html:19
|
||||
msgid "Circle information"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:39
|
||||
msgid "This circle has no group chat associated."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:44
|
||||
#: snikket_web/templates/admin_edit_circle.html:26
|
||||
msgid "Return to circle list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:49
|
||||
#: snikket_web/templates/admin_edit_circle.html:31
|
||||
msgid "Deleting a circle does not delete any users in the circle."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:55
|
||||
#: snikket_web/templates/admin_edit_circle.html:38
|
||||
msgid "Group chats"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:39
|
||||
msgid "These group chats will be available to all members of the circle."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:53
|
||||
#, python-format
|
||||
msgid "Delete group chat '%(name)s'"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:61
|
||||
msgid "This circle currently has no group chats."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:64
|
||||
msgid "Add group chat"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:67
|
||||
msgid "Circle members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:71
|
||||
#: snikket_web/templates/admin_edit_circle.html:68
|
||||
msgid "All members of the circle will see each other in their contact list."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:85
|
||||
msgid "The user has been deleted from the server."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:71
|
||||
#: snikket_web/templates/library.j2:108
|
||||
#: snikket_web/templates/admin_edit_circle.html:85
|
||||
#: snikket_web/templates/library.j2:131
|
||||
msgid "deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:77
|
||||
#: snikket_web/templates/admin_edit_circle.html:91
|
||||
#, python-format
|
||||
msgid "Remove user %(username)s from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:85
|
||||
#: snikket_web/templates/admin_edit_circle.html:99
|
||||
msgid "This circle currently has no members."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:87
|
||||
#: snikket_web/templates/admin_edit_circle.html:101
|
||||
msgid "Invite more members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:90
|
||||
#: snikket_web/templates/admin_edit_circle.html:104
|
||||
msgid "Add existing user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:101
|
||||
#: snikket_web/templates/admin_edit_circle.html:115
|
||||
msgid "All users added"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:102
|
||||
#: snikket_web/templates/admin_edit_circle.html:116
|
||||
msgid "All users on this service are already in this circle."
|
||||
msgstr ""
|
||||
|
||||
@@ -756,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 ""
|
||||
|
||||
@@ -996,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
|
||||
@@ -1093,20 +1218,20 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_register.html:14
|
||||
#: snikket_web/templates/invite_view.html:39
|
||||
#: snikket_web/templates/invite_view.html:46
|
||||
msgid "App already installed?"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_register.html:16
|
||||
#: snikket_web/templates/invite_reset_view.html:21
|
||||
#: snikket_web/templates/invite_view.html:41
|
||||
#: snikket_web/templates/invite_view.html:84
|
||||
#: snikket_web/templates/invite_view.html:112
|
||||
#: snikket_web/templates/invite_view.html:48
|
||||
#: snikket_web/templates/invite_view.html:91
|
||||
#: snikket_web/templates/invite_view.html:119
|
||||
msgid "Open the app"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_register.html:18
|
||||
#: snikket_web/templates/invite_view.html:43
|
||||
#: snikket_web/templates/invite_view.html:50
|
||||
msgid "This button works only if you have the app installed already!"
|
||||
msgstr ""
|
||||
|
||||
@@ -1135,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."
|
||||
@@ -1202,7 +1327,7 @@ msgid "You will then be prompted to enter a new password for your account."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_reset_view.html:29
|
||||
#: snikket_web/templates/invite_view.html:45
|
||||
#: snikket_web/templates/invite_view.html:52
|
||||
msgid "Alternatives"
|
||||
msgstr ""
|
||||
|
||||
@@ -1232,6 +1357,11 @@ msgstr ""
|
||||
msgid "Your address"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_success.html:15
|
||||
#: snikket_web/templates/user_home.html:21
|
||||
msgid "Copy address"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_success.html:17
|
||||
msgid ""
|
||||
"You can now set up your legacy XMPP client with the above address and the"
|
||||
@@ -1288,15 +1418,22 @@ msgid ""
|
||||
"privacy-friendly chat app."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:20
|
||||
#: snikket_web/templates/invite_view.html:23
|
||||
#, python-format
|
||||
msgid ""
|
||||
"By continuing, you agree to the <a href=\"%(tos_uri)s\">Terms of "
|
||||
"Service</a> and <a href=\"%(privacy_uri)s\">Privacy Policy</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:27
|
||||
msgid "Get started"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:22
|
||||
#: snikket_web/templates/invite_view.html:29
|
||||
msgid "Install the Snikket App on your Android or iOS device."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:24
|
||||
#: snikket_web/templates/invite_view.html:31
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Install the Snikket App on your Android device (<a "
|
||||
@@ -1304,30 +1441,30 @@ msgid ""
|
||||
"target=\"_blank\">iOS coming soon!</a>)."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:28
|
||||
#: snikket_web/templates/invite_view.html:35
|
||||
msgid "Get it on Google Play"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:30
|
||||
#: snikket_web/templates/invite_view.html:80
|
||||
#: snikket_web/templates/invite_view.html:37
|
||||
#: snikket_web/templates/invite_view.html:87
|
||||
msgid "Download on the App Store"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:32
|
||||
#: snikket_web/templates/invite_view.html:39
|
||||
msgid "Get it on F-Droid"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:35
|
||||
#: snikket_web/templates/invite_view.html:42
|
||||
msgid "Send to mobile device"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:38
|
||||
#: snikket_web/templates/invite_view.html:45
|
||||
msgid ""
|
||||
"After installation the app should automatically open and prompt you to "
|
||||
"create an account. If not, simply click the button below."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:46
|
||||
#: snikket_web/templates/invite_view.html:53
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You can connect to Snikket using any XMPP-compatible software. If the "
|
||||
@@ -1335,75 +1472,91 @@ msgid ""
|
||||
"href=\"%(register_url)s\">register an account manually</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:52
|
||||
#: snikket_web/templates/invite_view.html:59
|
||||
msgid "Scan invite code"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:55
|
||||
#: snikket_web/templates/invite_view.html:62
|
||||
#: snikket_web/templates/invite_view.html:74
|
||||
#: snikket_web/templates/invite_view.html:90
|
||||
#: snikket_web/templates/invite_view.html:102
|
||||
#: snikket_web/templates/invite_view.html:118
|
||||
#: snikket_web/templates/invite_view.html:69
|
||||
#: snikket_web/templates/invite_view.html:81
|
||||
#: snikket_web/templates/invite_view.html:97
|
||||
#: snikket_web/templates/invite_view.html:109
|
||||
#: snikket_web/templates/invite_view.html:125
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:58
|
||||
#: snikket_web/templates/invite_view.html:65
|
||||
msgid ""
|
||||
"You can transfer this invite to your mobile device by scanning a code "
|
||||
"with your camera. You can use either a QR scanner app or the Snikket app "
|
||||
"itself."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:71
|
||||
#: snikket_web/templates/invite_view.html:78
|
||||
msgid "Install on iOS"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:77
|
||||
#: snikket_web/templates/invite_view.html:84
|
||||
msgid ""
|
||||
"After downloading Snikket from the App Store, you have to return to this "
|
||||
"invite link and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:79
|
||||
#: snikket_web/templates/invite_view.html:86
|
||||
msgid "First download Snikket from the App Store using the button below:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:81
|
||||
#: snikket_web/templates/invite_view.html:109
|
||||
#: snikket_web/templates/invite_view.html:88
|
||||
#: snikket_web/templates/invite_view.html:116
|
||||
msgid ""
|
||||
"After the installation is complete, you can return to this page and tap "
|
||||
"the \"Open the app\" button to continue with the setup:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:99
|
||||
#: snikket_web/templates/invite_view.html:108
|
||||
#: snikket_web/templates/invite_view.html:106
|
||||
#: snikket_web/templates/invite_view.html:115
|
||||
msgid "Install via F-Droid"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:105
|
||||
#: snikket_web/templates/invite_view.html:112
|
||||
msgid ""
|
||||
"After installing Snikket via F-Droid, you have to return to this invite "
|
||||
"link and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:107
|
||||
#: snikket_web/templates/invite_view.html:114
|
||||
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 ""
|
||||
|
||||
@@ -1426,6 +1579,43 @@ msgid ""
|
||||
"<em>@%(snikket_domain)s</em>. Your password was not sent."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/policies.html:4 snikket_web/templates/policies.html:10
|
||||
msgid "Policies"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/policies.html:13
|
||||
msgid "Use of this service is subject to the following policies:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/policies.html:16
|
||||
msgid "Terms of Service"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/policies.html:19
|
||||
msgid "Privacy Policy"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/policies.html:23
|
||||
msgid ""
|
||||
"Please contact the administrator of this instance if you have questions "
|
||||
"about policies."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/policies.html:26
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use of the Snikket apps is subject to the <a href=\"%(url)s\">Snikket "
|
||||
"Apps Privacy Policy</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/policies.html:29
|
||||
#, python-format
|
||||
msgid ""
|
||||
"To report policy violations or other abuse from this service, please send"
|
||||
" an email to %(email)s. Specify the domain name of this instance "
|
||||
"(%(domain)s) and include details of the incident(s)."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/unauth.html:16
|
||||
msgid "Operation successful"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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
6
tools/import-icons.sh
Normal file → Executable 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 Google’s Material Icons set,\nlicensed under the terms of the Apache 2.0 License -->\n' >> "$output_file"
|
||||
|
||||
Reference in New Issue
Block a user