Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace per-post read tracker with per-thread one #1800

Merged
merged 30 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
aa07be7
Basic thread/category read tracking impl
rafalp Aug 24, 2024
e00e178
WIP new read tracker
rafalp Aug 26, 2024
ddc2f5a
Annotate category and thread querysets with users read times
rafalp Aug 26, 2024
4e346bd
Get category read status
rafalp Aug 26, 2024
4e1c250
Get category read status
rafalp Aug 26, 2024
a21a18b
New posts -> unread posts
rafalp Aug 26, 2024
674af24
Fix threads lists and replies pages showstoppers, rename new to unrea…
rafalp Aug 26, 2024
828a631
Cleanup tracker codebase a little, more tests
rafalp Aug 27, 2024
8740f79
Read tracker API change: get item unread posts -> get urnead items
rafalp Aug 27, 2024
429dbcc
Add tests for rendering unread threads on threads lists
rafalp Aug 28, 2024
d277fc8
Add tests for unread posts
rafalp Aug 28, 2024
45a4dda
WIP mark thread and category as read
rafalp Aug 28, 2024
538b92c
Fix devfixture timestamps breaking read tracker
rafalp Aug 28, 2024
4dbadbb
Mark thread as read
rafalp Aug 29, 2024
32d1ba8
Mark category read
rafalp Aug 29, 2024
3775c76
Test check for unread private threads
rafalp Aug 29, 2024
a97395b
Mark thread as read
rafalp Aug 29, 2024
c9e2039
Add unread threads filter, small improv of filters
rafalp Aug 30, 2024
9e5d407
First pass on mark all as read button
rafalp Aug 30, 2024
677af44
Mark as read flow templates
rafalp Aug 30, 2024
f1d70c7
Hide mark as read button on empty threads lists
rafalp Aug 30, 2024
41d4276
Test mark as read action
rafalp Aug 31, 2024
960ba28
Add mark as read to categories page, tweak mark as read action
rafalp Aug 31, 2024
b06417b
Move mark as read modals and pages to dedicated base templates
rafalp Aug 31, 2024
1744e11
Fix bug in readtracker, WIP mark category as read from its threads list
rafalp Sep 1, 2024
fd4abb8
Mark category as read by list view if category has no unread threads
rafalp Sep 3, 2024
8e510d8
Update user's watched thread entry on thread read
rafalp Sep 3, 2024
e318c08
Generate hooks reference
rafalp Sep 3, 2024
e95e26a
Fix migrations
rafalp Sep 3, 2024
06855bf
Fix some tests
rafalp Sep 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions dev-docs/plugins/hooks/get-category-threads-query-hook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# `get_category_threads_query_hook`

This hook wraps the standard function that Misago uses to get the name of the predefined database `WHERE` clause (represented as a `Q` object instance) to use to retrieve threads from the given category.


## Location

This hook can be imported from `misago.permissions.hooks`:

```python
from misago.permissions.hooks import get_category_threads_query_hook
```


## Filter

```python
def custom_get_category_threads_query_filter(
action: GetCategoryThreadsQueryHookAction,
permissions: 'UserPermissionsProxy',
category: dict,
) -> str | list[str] | None:
...
```

A function implemented by a plugin that can be registered in this hook.


### Arguments

#### `action: GetCategoryThreadsQueryHookAction`

A standard Misago function used to get the name of the predefined database `WHERE` clause (represented as a `Q` object instance) to use to retrieve threads from the given category.

See the [action](#action) section for details.


#### `user_permissions: UserPermissionsProxy`

A proxy object with the current user's permissions.


#### `category: dict`

A `dict` with category data.


### Return value

A `CategoryThreadsQuery` member or a `str` with a custom clause name. If `None`, the query retrieving threads will skip this category. If multiple clauses should be `OR`ed together, a list of strings or `CategoryThreadsQuery` members can be returned.


## Action

```python
def get_category_threads_query_action(
permissions: 'UserPermissionsProxy', category: dict
) -> str | list[str] | None:
...
```

A standard Misago function used to get the name of the predefined database `WHERE` clause (represented as a `Q` object instance) to use to retrieve threads from the given category.

Standard `WHERE` clauses implemented by Misago can be retrieved from the `CategoryThreadsQuery` `StrEnum`:

```python
from misago.permissions.enums import CategoryThreadsQuery
```


### Arguments

#### `user_permissions: UserPermissionsProxy`

A proxy object with the current user's permissions.


#### `category: dict`

A `dict` with category data.


### Return value

A `CategoryThreadsQuery` member or a `str` with a custom clause name. If `None`, the query retrieving threads will skip this category. If multiple clauses should be `OR`ed together, a list of strings or `CategoryThreadsQuery` members can be returned.


## Example

The code below implements a custom filter function that specifies a custom `WHERE` clause supported by the `get_threads_query_orm_filter_hook`.

```python
from misago.permissions.hooks import get_category_threads_query_hook
from misago.permissions.proxy import UserPermissionsProxy

@get_category_threads_query_hook.append_filter
def get_category_threads_query(
action,
permissions: UserPermissionsProxy,
category: dict,
) -> str | list[str] | None:
if (
category.get("plugin_flag") and context == CategoryQueryContext.CURRENT
):
return "plugin-where"

return action(permissions, category)
```
1 change: 1 addition & 0 deletions dev-docs/plugins/hooks/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Hooks instances are importable from the following Python modules:
- [`get_admin_category_permissions_hook`](./get-admin-category-permissions-hook.md)
- [`get_category_threads_category_query_hook`](./get-category-threads-category-query-hook.md)
- [`get_category_threads_pinned_category_query_hook`](./get-category-threads-pinned-category-query-hook.md)
- [`get_category_threads_query_hook`](./get-category-threads-query-hook.md)
- [`get_threads_category_query_hook`](./get-threads-category-query-hook.md)
- [`get_threads_pinned_category_query_hook`](./get-threads-pinned-category-query-hook.md)
- [`get_threads_query_orm_filter_hook`](./get-threads-query-orm-filter-hook.md)
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export class Misago {
bulkModeration(options) {
return new BulkModeration(options)
}

hideMarkAsReadModal = () => {
$("#mark-as-read").modal("hide")
}
}

// create the singleton
Expand All @@ -116,7 +120,7 @@ document.addEventListener("htmx:afterRequest", ({ target }) => {
}
})

// Hide moderation modal after moderation action completes
// Hide moderation modal
document.addEventListener("misago:afterModeration", () => {
$("#threads-moderation-modal").modal("hide")
})
2 changes: 1 addition & 1 deletion frontend/src/style/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
@import "misago/quote-control.less";
@import "misago/section-divider.less";
@import "misago/select-category-list.less";
@import "misago/thread-feed.less";
@import "misago/posts-feed.less";

// Pages
@import "misago/auth-pages.less";
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/style/misago/panels.less
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,8 @@
position: sticky;
bottom: 0;
}

.panel-prompt {
max-width: 500px;
margin: @line-height-computed auto;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.thread-feed {
.posts-feed {
margin-left: floor(@grid-gutter-width / -2);
margin-right: ceil(@grid-gutter-width / -2);
margin-bottom: @line-height-computed;
Expand All @@ -7,44 +7,44 @@
border-bottom: 1px solid @gray-lighter;
}

.thread-feed-item {
.posts-feed-item {
padding: @line-height-computed floor(@grid-gutter-width / 2);

border-bottom: 1px solid @gray-lighter;
}

.thread-feed-item:last-child {
.posts-feed-item:last-child {
border-bottom: 0;
}

.thread-feed-item-post {
.posts-feed-item-post {
display: flex;
}

.thread-feed-item-post-bit-side {
.posts-feed-item-post-bit-side {
display: none;
}

.thread-feed-item-post-bit-side-avatar {
.posts-feed-item-post-bit-side-avatar {
display: none;
}

.thread-feed-item-post-bit-side-avatar-sm {
.posts-feed-item-post-bit-side-avatar-sm {
display: none;
padding-right: floor(@grid-gutter-width / 2);
}

.thread-feed-item-post-bit-side-avatar-link {
.posts-feed-item-post-bit-side-avatar-link {
display: inline-block;
}

.thread-feed-item-post-bit-side-poster {
.posts-feed-item-post-bit-side-poster {
display: none;
padding-top: 3px;
width: 180px;
}

.thread-feed-item-post-bit-side-poster-link {
.posts-feed-item-post-bit-side-poster-link {
margin-bottom: floor(@line-height-computed / 2);

&,
Expand All @@ -60,7 +60,7 @@
}
}

.thread-feed-item-post-bit-side-poster-title {
.posts-feed-item-post-bit-side-poster-title {
&,
&:link,
&:visited,
Expand All @@ -71,20 +71,20 @@
}
}

.thread-feed-item-post-main {
.posts-feed-item-post-main {
width: 100%;
}

.thread-feed-item-post-bit-top {
.posts-feed-item-post-bit-top {
display: flex;
align-items: center;
}

.thread-feed-item-post-bit-top-avatar {
.posts-feed-item-post-bit-top-avatar {
margin-right: floor(@grid-gutter-width / 2);
}

.thread-feed-item-post-bit-top-poster-link {
.posts-feed-item-post-bit-top-poster-link {
margin-right: floor(@grid-gutter-width / 2);

&,
Expand All @@ -98,7 +98,7 @@
}
}

.thread-feed-item-post-bit-top-poster-title {
.posts-feed-item-post-bit-top-poster-title {
margin-right: floor(@grid-gutter-width / 2);

&,
Expand All @@ -111,9 +111,9 @@
}
}

.thread-feed-item-post-bit-top-timestamp,
.thread-feed-item-post-bit-top-timestamp-compact,
.thread-feed-item-post-bit-top-post-counter {
.posts-feed-item-post-bit-top-timestamp,
.posts-feed-item-post-bit-top-timestamp-compact,
.posts-feed-item-post-bit-top-post-counter {
margin-right: floor(@grid-gutter-width / 2);

&:link,
Expand All @@ -125,64 +125,74 @@
}
}

.thread-feed-item-post-bit-top-timestamp-compact {
.posts-feed-item-post-bit-top-timestamp-compact {
width: 100%;
}

.thread-feed-item-post-bit-top-timestamp {
.posts-feed-item-post-bit-top-timestamp {
display: none;
width: 100%;
}

.thread-feed-item-post-body-message {
.posts-feed-item-post-bit-top-post-new {
margin-right: 12px;
padding: 0 4px;

background-color: @brand-primary;
border-radius: @border-radius-small;
color: #fff;
font-size: @font-size-small;
}

.posts-feed-item-post-body-message {
padding: floor(@line-height-computed / 2) 0;
}

@media screen and (min-width: 500px) {
.thread-feed {
.posts-feed {
margin-left: 0;
margin-right: 0;
}

.thread-feed-item {
.posts-feed-item {
padding: @line-height-computed 0;
}

.thread-feed-item-post-bit-side,
.thread-feed-item-post-bit-side-avatar-sm {
.posts-feed-item-post-bit-side,
.posts-feed-item-post-bit-side-avatar-sm {
display: block;
}

.thread-feed-item-post-bit-top-avatar {
.posts-feed-item-post-bit-top-avatar {
display: none;
}
}

@media screen and (min-width: @screen-md-min) {
.thread-feed-item-post-bit-side {
.posts-feed-item-post-bit-side {
display: flex;
padding-left: 0;
}

.thread-feed-item-post-bit-side-avatar-sm,
.thread-feed-item-post-bit-top-poster-link,
.thread-feed-item-post-bit-top-poster-title,
.thread-feed-item-post-bit-top-timestamp-compact {
.posts-feed-item-post-bit-side-avatar-sm,
.posts-feed-item-post-bit-top-poster-link,
.posts-feed-item-post-bit-top-poster-title,
.posts-feed-item-post-bit-top-timestamp-compact {
display: none;
}

.thread-feed-item-post-bit-side-avatar,
.thread-feed-item-post-bit-side-poster {
.posts-feed-item-post-bit-side-avatar,
.posts-feed-item-post-bit-side-poster {
display: block;
}

.thread-feed-item-post-bit-side-poster {
.posts-feed-item-post-bit-side-poster {
padding-left: ceil(@grid-gutter-width / 2);
padding-right: @grid-gutter-width;
}


.thread-feed-item-post-bit-top-timestamp {
.posts-feed-item-post-bit-top-timestamp {
display: inline;
}
}
Loading
Loading