More UI!
This commit is contained in:
parent
5a8b6bb3d0
commit
34b24a0dcb
@ -1,4 +1,4 @@
|
|||||||
from typing import Dict
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import urlman
|
import urlman
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -126,10 +126,14 @@ class Post(StatorModel):
|
|||||||
### Local creation ###
|
### Local creation ###
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_local(cls, author: Identity, content: str) -> "Post":
|
def create_local(
|
||||||
|
cls, author: Identity, content: str, summary: Optional[str] = None
|
||||||
|
) -> "Post":
|
||||||
post = cls.objects.create(
|
post = cls.objects.create(
|
||||||
author=author,
|
author=author,
|
||||||
content=content,
|
content=content,
|
||||||
|
summary=summary or None,
|
||||||
|
sensitive=bool(summary),
|
||||||
local=True,
|
local=True,
|
||||||
)
|
)
|
||||||
post.object_uri = post.author.actor_uri + f"posts/{post.id}/"
|
post.object_uri = post.author.actor_uri + f"posts/{post.id}/"
|
||||||
|
0
activities/templatetags/__init__.py
Normal file
0
activities/templatetags/__init__.py
Normal file
33
activities/templatetags/activity_tags.py
Normal file
33
activities/templatetags/activity_tags.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def timedeltashort(value: datetime.datetime):
|
||||||
|
"""
|
||||||
|
A more compact version of timesince
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
# TODO: Handle things in the future properly
|
||||||
|
delta = timezone.now() - value
|
||||||
|
seconds = int(delta.total_seconds())
|
||||||
|
days = delta.days
|
||||||
|
if seconds < 60:
|
||||||
|
text = f"{seconds:0n}s"
|
||||||
|
elif seconds < 60 * 60:
|
||||||
|
minutes = seconds // 60
|
||||||
|
text = f"{minutes:0n}m"
|
||||||
|
elif seconds < 60 * 60 * 24:
|
||||||
|
hours = seconds // (60 * 60)
|
||||||
|
text = f"{hours:0n}h"
|
||||||
|
elif days < 365:
|
||||||
|
text = f"{days:0n}h"
|
||||||
|
else:
|
||||||
|
years = days // 365.25
|
||||||
|
text = f"{years:0n}y"
|
||||||
|
return text
|
@ -1,42 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.template.defaultfilters import linebreaks_filter
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.views.generic import FormView
|
|
||||||
|
|
||||||
from activities.models import Post, TimelineEvent
|
|
||||||
from core.forms import FormHelper
|
|
||||||
from users.decorators import identity_required
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(identity_required, name="dispatch")
|
|
||||||
class Home(FormView):
|
|
||||||
|
|
||||||
template_name = "activities/home.html"
|
|
||||||
|
|
||||||
class form_class(forms.Form):
|
|
||||||
text = forms.CharField()
|
|
||||||
|
|
||||||
helper = FormHelper(submit_text="Post")
|
|
||||||
|
|
||||||
def get_context_data(self):
|
|
||||||
context = super().get_context_data()
|
|
||||||
context.update(
|
|
||||||
{
|
|
||||||
"timeline_posts": [
|
|
||||||
te.subject_post
|
|
||||||
for te in TimelineEvent.objects.filter(
|
|
||||||
identity=self.request.identity,
|
|
||||||
type=TimelineEvent.Types.post,
|
|
||||||
).order_by("-created")[:100]
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return context
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
Post.create_local(
|
|
||||||
author=self.request.identity,
|
|
||||||
content=linebreaks_filter(form.cleaned_data["text"]),
|
|
||||||
)
|
|
||||||
return redirect(".")
|
|
70
activities/views/timelines.py
Normal file
70
activities/views/timelines.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.template.defaultfilters import linebreaks_filter
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.generic import FormView, TemplateView
|
||||||
|
|
||||||
|
from activities.models import Post, TimelineEvent
|
||||||
|
from users.decorators import identity_required
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(identity_required, name="dispatch")
|
||||||
|
class Home(FormView):
|
||||||
|
|
||||||
|
template_name = "activities/home.html"
|
||||||
|
|
||||||
|
class form_class(forms.Form):
|
||||||
|
text = forms.CharField(
|
||||||
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"placeholder": "What's on your mind?",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
content_warning = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": "Content Warning",
|
||||||
|
"class": "hidden",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
context = super().get_context_data()
|
||||||
|
context["timeline_posts"] = [
|
||||||
|
te.subject_post
|
||||||
|
for te in TimelineEvent.objects.filter(
|
||||||
|
identity=self.request.identity,
|
||||||
|
type=TimelineEvent.Types.post,
|
||||||
|
)
|
||||||
|
.select_related("subject_post", "subject_post__author")
|
||||||
|
.order_by("-created")[:100]
|
||||||
|
]
|
||||||
|
context["current_page"] = "home"
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
Post.create_local(
|
||||||
|
author=self.request.identity,
|
||||||
|
content=linebreaks_filter(form.cleaned_data["text"]),
|
||||||
|
summary=form.cleaned_data.get("content_warning"),
|
||||||
|
)
|
||||||
|
return redirect(".")
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(identity_required, name="dispatch")
|
||||||
|
class Federated(TemplateView):
|
||||||
|
|
||||||
|
template_name = "activities/federated.html"
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
context = super().get_context_data()
|
||||||
|
context["timeline_posts"] = (
|
||||||
|
Post.objects.filter(visibility=Post.Visibilities.public)
|
||||||
|
.select_related("author")
|
||||||
|
.order_by("-created")[:100]
|
||||||
|
)
|
||||||
|
context["current_page"] = "federated"
|
||||||
|
return context
|
@ -1,11 +0,0 @@
|
|||||||
from crispy_forms.helper import FormHelper as BaseFormHelper
|
|
||||||
from crispy_forms.layout import Submit
|
|
||||||
|
|
||||||
|
|
||||||
class FormHelper(BaseFormHelper):
|
|
||||||
|
|
||||||
submit_text = "Submit"
|
|
||||||
|
|
||||||
def __init__(self, form=None, submit_text=None):
|
|
||||||
super().__init__(form)
|
|
||||||
self.add_input(Submit("submit", submit_text or "Submit"))
|
|
@ -1,6 +1,6 @@
|
|||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from activities.views.home import Home
|
from activities.views.timelines import Home
|
||||||
from users.models import Identity
|
from users.models import Identity
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ django~=4.1
|
|||||||
pyld~=2.0.3
|
pyld~=2.0.3
|
||||||
pillow~=9.3.0
|
pillow~=9.3.0
|
||||||
urlman~=2.0.1
|
urlman~=2.0.1
|
||||||
django-crispy-forms~=1.14
|
|
||||||
cryptography~=38.0
|
cryptography~=38.0
|
||||||
httpx~=0.23
|
httpx~=0.23
|
||||||
pyOpenSSL~=22.1.0
|
pyOpenSSL~=22.1.0
|
||||||
|
@ -22,9 +22,6 @@ ignore_missing_imports = True
|
|||||||
[mypy-urlman.*]
|
[mypy-urlman.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-crispy_forms.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-cryptography.*]
|
[mypy-cryptography.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
@ -79,6 +79,7 @@ a {
|
|||||||
--color-bg-main: #26323c;
|
--color-bg-main: #26323c;
|
||||||
--color-bg-menu: #2e3e4c;
|
--color-bg-menu: #2e3e4c;
|
||||||
--color-bg-box: #1a2631;
|
--color-bg-box: #1a2631;
|
||||||
|
--color-bg-error: rgb(87, 32, 32);
|
||||||
--color-highlight: #449c8c;
|
--color-highlight: #449c8c;
|
||||||
|
|
||||||
--color-text-duller: #5f6983;
|
--color-text-duller: #5f6983;
|
||||||
@ -90,13 +91,13 @@ a {
|
|||||||
--color-button-main: #444b5d;
|
--color-button-main: #444b5d;
|
||||||
--color-button-main-hover: #515d7c;
|
--color-button-main-hover: #515d7c;
|
||||||
--color-bg3: #444b5d;
|
--color-bg3: #444b5d;
|
||||||
--color-text-error: rgb(155, 111, 111);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--color-bg-main);
|
background-color: var(--color-bg-main);
|
||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
font-family: "Raleway", sans-serif;
|
font-family: "Raleway", sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@ -123,7 +124,7 @@ header .logo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header .logo:hover {
|
header .logo:hover {
|
||||||
border-bottom: 3px solid rgba(255, 255, 255, 0.2);
|
border-bottom: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
header .logo img {
|
header .logo img {
|
||||||
@ -201,6 +202,33 @@ nav a:hover {
|
|||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Left-right columns */
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-column {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 300px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-column {
|
||||||
|
width: 250px;
|
||||||
|
background: var(--color-bg-menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-column h2 {
|
||||||
|
background: var(--color-highlight);
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 90%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon menus */
|
||||||
|
|
||||||
.icon-menu {
|
.icon-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -255,43 +283,111 @@ nav a:hover {
|
|||||||
|
|
||||||
/* Forms */
|
/* Forms */
|
||||||
|
|
||||||
form .control-group {
|
form {
|
||||||
margin: 0 0 15px 0;
|
padding: 20px 40px 20px 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form .asteriskField {
|
.right-column form {
|
||||||
display: none;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form h1 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form p {
|
||||||
|
color: var(--color-text-main);
|
||||||
|
margin: 10px 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .field {
|
||||||
|
margin: 25px 0 25px 0;
|
||||||
|
background: var(--color-bg-box);
|
||||||
|
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-column form .field {
|
||||||
|
margin: 0;
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form label {
|
form label {
|
||||||
|
display: block;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 110%;
|
font-size: 100%;
|
||||||
color: var(--color-text-dull);
|
font-weight: bold;
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
form label.requiredField::after {
|
form label small {
|
||||||
content: " (required)";
|
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
color: var(--color-text-duller);
|
margin-left: 5px;
|
||||||
|
color: var(--color-text-dull);
|
||||||
}
|
}
|
||||||
|
|
||||||
form .help-block {
|
.right-column form label {
|
||||||
color: var(--color-text-error);
|
margin: 5px 10px 5px 10px;
|
||||||
padding: 4px 0 0 0;
|
}
|
||||||
|
|
||||||
|
form .help {
|
||||||
|
color: var(--color-text-dull);
|
||||||
|
font-size: 90%;
|
||||||
|
margin: 2px 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .errorlist {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .errorlist li {
|
||||||
|
color: var(--color-text-main);
|
||||||
|
background: var(--color-bg-error);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 5px 0 8px 0;
|
||||||
|
padding: 3px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .errorlist li::before {
|
||||||
|
content: "\f071";
|
||||||
|
font: var(--fa-font-solid);
|
||||||
|
margin-right: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .hidden {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input,
|
form input,
|
||||||
form select {
|
form select,
|
||||||
|
form textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 4px 6px;
|
padding: 5px 7px;
|
||||||
background: var(--color-bg-main);
|
background: var(--color-bg-main);
|
||||||
border: 1px solid var(--color-input-border);
|
border: 1px solid var(--color-input-border);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
form input:focus {
|
.right-column form.compose input,
|
||||||
|
.right-column form.compose textarea {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 95%;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: var(--color-bg-box);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-column form.compose textarea {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input:focus,
|
||||||
|
form select:focus,
|
||||||
|
form textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 1px solid var(--color-input-border-active);
|
border: 1px solid var(--color-input-border-active);
|
||||||
}
|
}
|
||||||
@ -311,6 +407,48 @@ form input[type=submit]:hover {
|
|||||||
background: var(--color-button-main-hover);
|
background: var(--color-button-main-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form .buttons {
|
||||||
|
text-align: right;
|
||||||
|
margin: 25px 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-column form .buttons {
|
||||||
|
margin: 5px 10px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button,
|
||||||
|
form .button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin: 0 0 0 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: var(--color-highlight);
|
||||||
|
color: var(--color-text-main);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button.toggle,
|
||||||
|
form .button.toggle {
|
||||||
|
background: var(--color-bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
form button.toggle.enabled,
|
||||||
|
form .button.toggle.enabled {
|
||||||
|
background: var(--color-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
form button:hover,
|
||||||
|
form .button:hover {
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-column form button,
|
||||||
|
.right-column form .button {
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Identities */
|
/* Identities */
|
||||||
|
|
||||||
h1.identity {
|
h1.identity {
|
||||||
@ -350,29 +488,26 @@ h1.identity small {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left-column .post {
|
||||||
|
background: var(--color-bg-box);
|
||||||
|
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.post .icon {
|
.post .icon {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
width: auto;
|
width: auto;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post .author {
|
.post .handle {
|
||||||
padding-left: 64px;
|
display: block;
|
||||||
}
|
padding: 7px 0 10px 64px;
|
||||||
|
|
||||||
.post .author a,
|
|
||||||
.post time a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post .author small {
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--color-text-dull);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.post time {
|
.post time {
|
||||||
display: block;
|
display: block;
|
||||||
|
float: right;
|
||||||
padding-left: 64px;
|
padding-left: 64px;
|
||||||
color: var(--color-text-duller);
|
color: var(--color-text-duller);
|
||||||
}
|
}
|
||||||
|
1
static/js/hyperscript.min.js
vendored
Executable file
1
static/js/hyperscript.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
@ -12,7 +12,6 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"crispy_forms",
|
|
||||||
"core",
|
"core",
|
||||||
"activities",
|
"activities",
|
||||||
"users",
|
"users",
|
||||||
|
@ -10,7 +10,6 @@ MIDDLEWARE.insert(0, "core.middleware.AlwaysSecureMiddleware")
|
|||||||
|
|
||||||
# Ensure debug features are on
|
# Ensure debug features are on
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
CRISPY_FAIL_SILENTLY = False
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
@ -11,7 +11,6 @@ except KeyError:
|
|||||||
|
|
||||||
# Ensure debug features are off
|
# Ensure debug features are off
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
CRISPY_FAIL_SILENTLY = True
|
|
||||||
|
|
||||||
# TODO: Allow better setting of allowed_hosts, if we need to
|
# TODO: Allow better setting of allowed_hosts, if we need to
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from activities.views import timelines
|
||||||
from core import views as core
|
from core import views as core
|
||||||
from stator import views as stator
|
from stator import views as stator
|
||||||
from users.views import activitypub, auth, identity
|
from users.views import activitypub, auth, identity
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", core.homepage),
|
path("", core.homepage),
|
||||||
|
# Activity views
|
||||||
|
path("federated/", timelines.Federated.as_view()),
|
||||||
# Authentication
|
# Authentication
|
||||||
path("auth/login/", auth.Login.as_view()),
|
path("auth/login/", auth.Login.as_view()),
|
||||||
path("auth/logout/", auth.Logout.as_view()),
|
path("auth/logout/", auth.Logout.as_view()),
|
||||||
|
6
templates/activities/_home_menu.html
Normal file
6
templates/activities/_home_menu.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<nav>
|
||||||
|
<a href="/" {% if current_page == "home" %}class="selected"{% endif %}>Home</a>
|
||||||
|
<a href="/" {% if current_page == "mentions" %}class="selected"{% endif %}>Mentions</a>
|
||||||
|
<a href="/" {% if current_page == "public" %}class="selected"{% endif %}>Public</a>
|
||||||
|
<a href="/federated/" {% if current_page == "federated" %}class="selected"{% endif %}>Federated</a>
|
||||||
|
</nav>
|
@ -1,4 +1,5 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load activity_tags %}
|
||||||
<div class="post">
|
<div class="post">
|
||||||
|
|
||||||
{% if post.author.icon_uri %}
|
{% if post.author.icon_uri %}
|
||||||
@ -7,20 +8,20 @@
|
|||||||
<img src="{% static "img/unknown-icon-128.png" %}" class="icon">
|
<img src="{% static "img/unknown-icon-128.png" %}" class="icon">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h3 class="author">
|
|
||||||
<a href="{{ post.author.urls.view }}">
|
|
||||||
{{ post.author.name_or_handle }} <small>@{{ post.author.handle }}</small>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<time>
|
<time>
|
||||||
<a href="{{ post.urls.view }}">
|
<a href="{{ post.urls.view }}">
|
||||||
{% if post.authored %}
|
{% if post.authored %}
|
||||||
{{ post.authored | timesince }} ago
|
{{ post.authored | timedeltashort }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ post.created | timesince }} ago
|
{{ post.created | timedeltashort }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</time>
|
</time>
|
||||||
|
|
||||||
|
<a href="{{ post.author.urls.view }}" class="handle">
|
||||||
|
{{ post.author.name_or_handle }} <small>@{{ post.author.handle }}</small>
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ post.safe_content }}
|
{{ post.safe_content }}
|
||||||
</div>
|
</div>
|
||||||
|
21
templates/activities/federated.html
Normal file
21
templates/activities/federated.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Federated Timeline{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "activities/_home_menu.html" %}
|
||||||
|
|
||||||
|
<section class="columns">
|
||||||
|
<div class="left-column">
|
||||||
|
{% for post in timeline_posts %}
|
||||||
|
{% include "activities/_post.html" %}
|
||||||
|
{% empty %}
|
||||||
|
No posts yet.
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="right-column">
|
||||||
|
<h2>?</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
@ -1,15 +1,32 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block title %}Home{% endblock %}
|
{% block title %}Home{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% include "activities/_home_menu.html" %}
|
||||||
|
|
||||||
{% crispy form form.helper %}
|
<section class="columns">
|
||||||
|
|
||||||
|
<div class="left-column">
|
||||||
{% for post in timeline_posts %}
|
{% for post in timeline_posts %}
|
||||||
{% include "activities/_post.html" %}
|
{% include "activities/_post.html" %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
No posts yet.
|
No posts yet.
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-column">
|
||||||
|
<h2>Compose</h2>
|
||||||
|
<form action="." method="POST" class="compose">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.text }}
|
||||||
|
{{ form.content_warning }}
|
||||||
|
<div class="buttons">
|
||||||
|
<span class="button toggle" _="on click toggle .enabled then toggle .hidden on #id_content_warning">CW</span>
|
||||||
|
<button>Post</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block title %}Login{% endblock %}
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="modal identities">
|
<nav>
|
||||||
<h1>Login</h1>
|
<a href="." class="selected">Login</a>
|
||||||
{% crispy form form.helper %}
|
</nav>
|
||||||
</section>
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% include "forms/_field.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="buttons">
|
||||||
|
<button>Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
<link rel="stylesheet" href="{% static "css/style.css" %}" type="text/css" media="screen" />
|
<link rel="stylesheet" href="{% static "css/style.css" %}" type="text/css" media="screen" />
|
||||||
<link rel="stylesheet" href="{% static "fonts/raleway/raleway.css" %}" type="text/css" />
|
<link rel="stylesheet" href="{% static "fonts/raleway/raleway.css" %}" type="text/css" />
|
||||||
<link rel="stylesheet" href="{% static "fonts/font_awesome/all.min.css" %}" type="text/css" />
|
<link rel="stylesheet" href="{% static "fonts/font_awesome/all.min.css" %}" type="text/css" />
|
||||||
|
<script src="{% static "js/hyperscript.min.js" %}"></script>
|
||||||
{% block extra_head %}{% endblock %}
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="{% block body_class %}{% endblock %}">
|
<body class="{% block body_class %}{% endblock %}">
|
||||||
|
13
templates/forms/_field.html
Normal file
13
templates/forms/_field.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div class="field">
|
||||||
|
<label for="{{ field.id_for_label }}">
|
||||||
|
{{ field.label }}
|
||||||
|
{% if field.field.required %}<small>(Required)</small>{% endif %}
|
||||||
|
</label>
|
||||||
|
{% if field.help_text %}
|
||||||
|
<p class="help">
|
||||||
|
{{ field.help_text }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{{ field.errors }}
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
@ -1,13 +1,19 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block title %}Create Identity{% endblock %}
|
{% block title %}Create Identity{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include "identity/_identity_menu.html" %}
|
{% include "identity/_identity_menu.html" %}
|
||||||
<section class="modal identities">
|
<form action="." method="POST">
|
||||||
<h1>Create Identity</h1>
|
<h1>Create New Identity</h1>
|
||||||
{% crispy form form.helper %}
|
<p>You can have multiple identities - they are totally separate, and share
|
||||||
</section>
|
nothing apart from your login details. Use them for alternates, projects, and more.</p>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% include "forms/_field.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="buttons">
|
||||||
|
<button>Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
{% block title %}Welcome{% endblock %}
|
{% block title %}Welcome{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<nav>
|
||||||
|
<a href="/" class="selected">Home</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
{% for identity in identities %}
|
{% for identity in identities %}
|
||||||
<a href="{{ identity.urls.view }}">{{ identity }}</a>
|
<a href="{{ identity.urls.view }}">{{ identity }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
from django.contrib.auth.forms import AuthenticationForm
|
|
||||||
from django.contrib.auth.views import LoginView, LogoutView
|
from django.contrib.auth.views import LoginView, LogoutView
|
||||||
|
|
||||||
from core.forms import FormHelper
|
|
||||||
|
|
||||||
|
|
||||||
class Login(LoginView):
|
class Login(LoginView):
|
||||||
class form_class(AuthenticationForm):
|
|
||||||
helper = FormHelper(submit_text="Login")
|
|
||||||
|
|
||||||
template_name = "auth/login.html"
|
template_name = "auth/login.html"
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views.generic import FormView, TemplateView, View
|
from django.views.generic import FormView, TemplateView, View
|
||||||
|
|
||||||
from core.config import Config
|
from core.config import Config
|
||||||
from core.forms import FormHelper
|
|
||||||
from users.decorators import identity_required
|
from users.decorators import identity_required
|
||||||
from users.models import Domain, Follow, Identity, IdentityStates
|
from users.models import Domain, Follow, Identity, IdentityStates
|
||||||
from users.shortcuts import by_handle_or_404
|
from users.shortcuts import by_handle_or_404
|
||||||
@ -83,28 +82,31 @@ class CreateIdentity(FormView):
|
|||||||
template_name = "identity/create.html"
|
template_name = "identity/create.html"
|
||||||
|
|
||||||
class form_class(forms.Form):
|
class form_class(forms.Form):
|
||||||
username = forms.CharField()
|
username = forms.CharField(
|
||||||
name = forms.CharField()
|
help_text="Must be unique on your domain. Cannot be changed easily. Use only: a-z 0-9 _ -"
|
||||||
|
)
|
||||||
helper = FormHelper(submit_text="Create")
|
domain = forms.ChoiceField(
|
||||||
|
help_text="Pick the domain to make this identity on. Cannot be changed later."
|
||||||
|
)
|
||||||
|
name = forms.CharField(
|
||||||
|
help_text="The display name other users see. You can change this easily."
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["domain"] = forms.ChoiceField(
|
self.fields["domain"].choices = [
|
||||||
choices=[
|
|
||||||
(domain.domain, domain.domain)
|
(domain.domain, domain.domain)
|
||||||
for domain in Domain.available_for_user(user)
|
for domain in Domain.available_for_user(user)
|
||||||
]
|
]
|
||||||
)
|
|
||||||
|
|
||||||
def clean_username(self):
|
def clean_username(self):
|
||||||
# Remove any leading @
|
# Remove any leading @ and force it lowercase
|
||||||
value = self.cleaned_data["username"].lstrip("@")
|
value = self.cleaned_data["username"].lstrip("@").lower()
|
||||||
# Validate it's all ascii characters
|
# Validate it's all ascii characters
|
||||||
for character in value:
|
for character in value:
|
||||||
if character not in string.ascii_letters + string.digits + "_-":
|
if character not in string.ascii_letters + string.digits + "_-":
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Only the letters a-z, numbers 0-9, dashes and underscores are allowed."
|
"Only the letters a-z, numbers 0-9, dashes, and underscores are allowed."
|
||||||
)
|
)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user