Skip to content

Commit

Permalink
Merge branch 'main' into i1449_docker_compose_quote_your_sources_bro
Browse files Browse the repository at this point in the history
  • Loading branch information
philpem authored Oct 31, 2024
2 parents 2ee0b54 + 211bd16 commit 286318b
Show file tree
Hide file tree
Showing 16 changed files with 442 additions and 207 deletions.
40 changes: 40 additions & 0 deletions assets/js/message-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {byClass, make} from './dom.js';

const notesList = byClass('notes-list');
const actionRemove = byClass('notes-action-remove');
const totalCount = document.getElementsByName('notes').length;
let checkedCount = notesList.querySelectorAll('[name=notes]:checked').length;
const selectAll = make('input', {
type: 'checkbox',
title: 'Select all on this page',
});

const updateSelection = () => {
selectAll.checked = checkedCount === totalCount;
selectAll.indeterminate = checkedCount > 0 && checkedCount < totalCount;
actionRemove.disabled = checkedCount === 0;
actionRemove.textContent = `Delete ${checkedCount} selected note${checkedCount === 1 ? '' : 's'}`;
};

selectAll.addEventListener('change', () => {
checkedCount = 0;

for (const noteSelect of document.getElementsByName('notes')) {
checkedCount += (noteSelect.checked = selectAll.checked);
}

updateSelection();
});

notesList.addEventListener('change', e => {
if (e.target.name === 'notes') {
checkedCount += e.target.checked ? 1 : -1;
updateSelection();
}
});

updateSelection();

if (!byClass('notes-empty')) {
byClass('notes-select-all').append(selectAll);
}
6 changes: 0 additions & 6 deletions assets/js/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,6 @@
});

// submission notifs buttons
$('.selection-toggle').on('click', function (ev) {
ev.preventDefault();
$(this).closest('form').find('input[type=checkbox]').each(function () {
this.checked = !this.checked;
}).change();
});
$('.do-check').on('click', function (ev) {
ev.preventDefault();
$(this).closest('form').find('input[type=checkbox]').prop('checked', true).change();
Expand Down
1 change: 1 addition & 0 deletions assets/scss/_theme.scss
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
$content-color-lighter: #777;
$link-color: #336979;
12 changes: 12 additions & 0 deletions assets/scss/components/_page-navigation.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.pages-layout-split {
display: flex;

> [rel=next] {
margin-left: auto;
}
}

// XXX: temporary hack for private message list
.pages-layout-left {
float: left;
}
95 changes: 95 additions & 0 deletions assets/scss/components/_private-messages.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
@import '../theme';

.notes-folders {
line-height: 1;
margin-bottom: 16px;

> ul {
display: flex;
flex-wrap: wrap;
gap: 8px;
}

li {
display: block;
}

a {
border-radius: 2px;
display: inline-block;
padding: 8px;
}

.current {
background-color: $link-color;
color: white;
text-decoration: none;
}
}

.notes-filter {
margin-bottom: 16px;
}

$cell-padding: 4px;

.notes-list > * > * {
// allow clicking anywhere on the row, outside a link, to select a message
> * > label {
display: block;
padding: $cell-padding;
}

> th {
padding: $cell-padding;
text-align: left;
vertical-align: bottom; // ideally, this would be middle-aligned text aligned to the bottom of the cell (for when some other cell's text wraps to multiple lines), but normally the checkbox is close in height, so good enough
}

// checkbox
> :nth-child(1) {
text-align: center;
width: 2em;
}

// message title
//
// desirable properties:
// - [X] no zalgo overflow
// - [X] focus ring normal-looking and unclipped
// - [X] link area doesn’t include empty space when text wraps
// - [ ] link area extends to full height of cell
// - [ ] link area exists even when title is zero-sized, e.g. contains no printing characters
> :nth-child(2) > label {
overflow: hidden;
}
}

.notes-list > tbody > :nth-child(odd) {
background-color: rgba(0, 0, 0, 0.05);
}

.notes-list .unread {
font-weight: bold;
}

.notes-empty {
padding: $cell-padding;
}

#note-content {
padding-top: 2em;
}

#note-content .formatted-content {
padding-top: 1.5em;
}

#notes-content {
font-size: 14px;
padding-top: 2em;
}

#notes-content .buttons {
padding-top: 1em;
}
34 changes: 8 additions & 26 deletions assets/scss/site.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
@import 'reset';
@import 'components/date-picker';
@import 'components/option';
@import 'components/page-navigation';
@import 'components/private-messages';
@import 'components/text-post-list';
@import 'components/user-tabs';
@import 'pages/marketplace';
Expand Down Expand Up @@ -109,7 +111,6 @@ i, #header-guest {
--------------------------------*/
$color-a: #069bc0;
$color-b: #8cad47;
$link-color: #336979;

.color-a, .username, .content .username, .page-title, #header-nav .current,
#user-nav .current,
Expand Down Expand Up @@ -674,7 +675,12 @@ dl + h4, #uc-info {

/* ------------------------------------ =inputs -- */

.button, .stage .content .button {
// Even when the surrounding text is a different size, keep default button style consistent – extending the existing `<button>` default to `<a class="button">`.
.button {
@extend .typeface-default;
}

.button, .content .button, .stage .content .button {
display: inline-block;
vertical-align: baseline;
padding: 0.4em 1.2em;
Expand Down Expand Up @@ -3895,30 +3901,6 @@ img + #detail-art-text {
padding-top: 2em;
}

#note-content {
padding-top: 2em;
}

#note-content .formatted-content {
padding-top: 1.5em;
}

#notes-content {
padding-top: 2em;
}

#notes-content th {
text-align: left;
}

#notes-content .unread {
font-weight: bold;
}

#notes-content .buttons {
padding-top: 1em;
}

/* ------------------------------------ =footer -- */

#footer {
Expand Down
1 change: 1 addition & 0 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ const main = async () => {
sasscFile('scss/signup.scss', 'css/signup.css', touch, copyImages),
esbuildFile('js/scripts.js', 'js/scripts.js', touch, {}),
esbuildFile('js/main.js', 'js/main.js', touch, PRIVATE_FIELDS_ESM),
esbuildFile('js/message-list.js', 'js/message-list.js', touch, PRIVATE_FIELDS_ESM),
esbuildFile('js/tags-edit.js', 'js/tags-edit.js', touch, PRIVATE_FIELDS_ESM),
esbuildFile('js/flash.js', 'js/flash.js', touch, {
format: 'esm',
Expand Down
1 change: 1 addition & 0 deletions libweasyl/models/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ def default_fkey(*args, **kwargs):
Column('noteid', Integer(), primary_key=True, nullable=False),
Column('userid', Integer(), nullable=False),
Column('otherid', Integer(), nullable=False),
# TODO: delete unused columns after deployment of change that removes them from queries
Column('user_folder', Integer(), nullable=False, server_default='0'),
Column('other_folder', Integer(), nullable=False, server_default='0'),
Column('title', String(length=100), nullable=False),
Expand Down
50 changes: 29 additions & 21 deletions weasyl/controllers/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from weasyl.controllers.decorators import login_required, token_checked
from weasyl.error import WeasylError
from weasyl import (
define, favorite, followuser, frienduser, ignoreuser, note, profile)
define, favorite, followuser, frienduser, ignoreuser, note, pagination, profile)


# User interactivity functions
Expand Down Expand Up @@ -114,27 +114,35 @@ def notes_(request):
raise WeasylError("vouchRequired")

form = request.web_input(folder="inbox", filter="", backid="", nextid="")
backid = int(form.backid) if form.backid else None
nextid = int(form.nextid) if form.nextid else None
filter_ = define.get_userid_list(form.filter)

if form.folder == "inbox":
return Response(define.webpage(request.userid, "note/message_list.html", [
# Folder
"inbox",
# Private messages
note.select_inbox(request.userid, 50, backid=backid, nextid=nextid, filter=filter_),
]))

if form.folder == "outbox":
return Response(define.webpage(request.userid, "note/message_list.html", [
# Folder
"outbox",
# Private messages
note.select_outbox(request.userid, 50, backid=backid, nextid=nextid, filter=filter_),
]))
select_list = note.select_inbox
select_count = note.select_inbox_count
title = "Inbox"
elif form.folder == "outbox":
select_list = note.select_outbox
select_count = note.select_outbox_count
title = "Sent messages"
else:
raise WeasylError("unknownMessageFolder")

raise WeasylError("unknownMessageFolder")
backid = int(form.backid) if form.backid else None
nextid = int(form.nextid) if form.nextid else None
filter_ = define.get_userids(define.get_sysname_list(form.filter))

result = pagination.PaginatedResult(
select_list, select_count, "noteid", f"/notes?folder={form.folder}&%s",
request.userid, filter=list(set(filter_.values())),
backid=backid,
nextid=nextid,
count_limit=note.COUNT_LIMIT,
)
return Response(define.webpage(request.userid, "note/message_list.html", (
form.folder,
result,
[(sysname, userid != 0) for sysname, userid in filter_.items()],
note.unread_count(request.userid),
), title=title))


@login_required
Expand All @@ -146,9 +154,9 @@ def notes_compose_get_(request):

return Response(define.webpage(request.userid, "note/compose.html", [
# Recipient
form.recipient.strip(),
"; ".join(define.get_sysname_list(form.recipient)),
profile.select_myself(request.userid),
]))
], title="Compose message"))


@login_required
Expand Down
15 changes: 11 additions & 4 deletions weasyl/define.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,12 @@ def get_userids(usernames):
return ret


def get_sysname_list(s: str) -> list[str]:
return list(filter(None, map(get_sysname, s.split(";"))))


def get_userid_list(target):
usernames = target.split(";")
return [userid for userid in get_userids(usernames).values() if userid != 0]
return [userid for userid in get_userids(get_sysname_list(target)).values() if userid != 0]


def get_ownerid(submitid=None, charid=None, journalid=None):
Expand Down Expand Up @@ -627,6 +630,11 @@ def posts_count(userid, *, friends: bool):
return result


def private_messages_unread_count(userid: int) -> int:
return engine.scalar(
"SELECT COUNT(*) FROM message WHERE otherid = %(user)s AND settings ~ 'u'", user=userid)


notification_count_time = metrics.CachedMetric(Histogram("weasyl_notification_count_fetch_seconds", "notification counts fetch time", ["cached"]))


Expand All @@ -635,8 +643,7 @@ def posts_count(userid, *, friends: bool):
@region.cache_on_arguments(expiration_time=180)
@notification_count_time.uncached
def _page_header_info(userid):
messages = engine.scalar(
"SELECT COUNT(*) FROM message WHERE otherid = %(user)s AND settings ~ 'u'", user=userid)
messages = private_messages_unread_count(userid)
result = [messages, 0, 0, 0, 0]

counts = engine.execute(
Expand Down
2 changes: 1 addition & 1 deletion weasyl/errorcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
"priceidInvalid": "You did not specify a price to edit.",
"RatingExceeded": rating,
"ratingInvalid": "The specified rating is invalid.",
"recipientExcessive": "You specified too many recipients for this message.",
"recipientExcessive": "Private messages can only be sent to one recipient at a time.",
"recipientInvalid": (
"Your message could not be delivered because you did not specify any valid recipients. The users "
"you specified may restrict who can send them private messages, or you might be on their ignore list."),
Expand Down
Loading

0 comments on commit 286318b

Please sign in to comment.