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

Categories display improvements #1399

Merged
merged 18 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ With exception of Admin Panel, Misago frontend relies heavily on React.js compon
Currently Misago defines following taks:

* **npm run build** does production build of Misago's assets, concating and minifying javascripts, css and images, as well as moving them to misago/static directory.
* **npm run watch** does quick build for assets (concat assets into single files, compile less, deploy to misago/static but don't minify/optimize) as well as runs re-build when less/js changes.
* **npm run start** does quick build for assets (concat assets into single files, compile less, deploy to misago/static but don't minify/optimize) as well as runs re-build when less/js changes.
* **npm run eslint** lints code with eslint.

To start work on custom frontend for Misago, fork and install it locally to have development forum setup. You can now develop custom theme by modifying assets in ``frontend`` directory, however special care should be taken when changing source javascripts.
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": ".",
"scripts": {
"build": "webpack --mode production --progress",
"watch": "webpack --watch --progress",
"start": "webpack --watch --progress",
"eslint": "eslint src"
},
"author": "Rafal Piton",
Expand Down
62 changes: 62 additions & 0 deletions frontend/src/components/ThreadsList/ThreadsList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from "react"
import ThreadsListEmpty from "./ThreadsListEmpty";
import ThreadsListItem from "./ThreadsListItem";
import ThreadsListLoader from "./ThreadsListLoader";
import ThreadsListUpdatePrompt from "./ThreadsListUpdatePrompt"

const ThreadsList = ({
list,
categories,
category,
threads,
busyThreads,
selection,
isLoaded,
showOptions,
updatedThreads,
applyUpdate,
emptyMessage,
}) => {
if (!isLoaded) {
return <ThreadsListLoader showOptions={showOptions} />
}

return (
<div className="threads-list">
{threads.length > 0 ? (
<ul className="list-group">
{updatedThreads > 0 && (
<ThreadsListUpdatePrompt threads={updatedThreads} onClick={applyUpdate} />
)}
{threads.map(
thread => (
<ThreadsListItem
key={thread.id}
activeCategory={category}
categories={categories}
thread={thread}
showOptions={showOptions}
showSubscription={showOptions && list.type === "subscribed"}
isBusy={busyThreads.indexOf(thread.id) >= 0}
isSelected={selection.indexOf(thread.id) >= 0}
/>
)
)}
</ul>
) : (
<ul className="list-group">
{updatedThreads > 0 && (
<ThreadsListUpdatePrompt threads={updatedThreads} onClick={applyUpdate} />
)}
<ThreadsListEmpty
category={category}
list={list}
message={emptyMessage}
/>
</ul>
)}
</div>
)
}

export default ThreadsList
35 changes: 35 additions & 0 deletions frontend/src/components/ThreadsList/ThreadsListEmpty.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react"

const ThreadsListEmpty = ({ category, list, message }) => {
if (list.type === "all") {
if (message) {
return (
<li className="list-group-item empty-message">
<p className="lead">{message}</p>
<p>{gettext("Why not start one yourself?")}</p>
</li>
)
}

return (
<li className="list-group-item empty-message">
<p className="lead">
{category.special_role
? gettext("There are no threads on this forum... yet!")
: gettext("There are no threads in this category.")}
</p>
<p>{gettext("Why not start one yourself?")}</p>
</li>
)
}

return (
<li className="list-group-item empty-message">
<p className="lead">
{gettext("No threads matching specified criteria were found.")}
</p>
</li>
)
}

export default ThreadsListEmpty
123 changes: 123 additions & 0 deletions frontend/src/components/ThreadsList/ThreadsListItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from "react"
import ThreadsListItemActivity from "./ThreadsListItemActivity"
import ThreadsListItemCategory from "./ThreadsListItemCategory"
import ThreadsListItemCheckbox from "./ThreadsListItemCheckbox"
import ThreadsListItemFlags from "./ThreadsListItemFlags"
import ThreadsListItemIcon from "./ThreadsListItemIcon"
import ThreadsListItemLastPoster from "./ThreadsListItemLastPoster"
import ThreadsListItemReplies from "./ThreadsListItemReplies"
import ThreadsListItemSubscription from "./ThreadsListItemSubscription"

const ThreadsListItem = ({
activeCategory,
categories,
showOptions,
showSubscription,
thread,
isBusy,
isSelected,
}) => {
let parent = null
let category = null

if (activeCategory.id !== thread.category) {
category = categories[thread.category]

if (
category.parent &&
category.parent !== activeCategory.id &&
categories[category.parent] &&
!categories[category.parent].special_role
) {
parent = categories[category.parent]
}
}

const hasFlags = (
thread.is_closed ||
thread.is_hidden ||
thread.is_unapproved ||
thread.weight > 0 ||
thread.best_answer ||
thread.has_poll ||
thread.has_unapproved_posts
)

const isNew = showOptions ? thread.is_new : true

return (
<li
className={
"list-group-item threads-list-item" + (isBusy ? " threads-list-item-is-busy" : "")
}
>
<div className="threads-list-item-top-row">
{showOptions && (
<div className="threads-list-item-col-icon">
<ThreadsListItemIcon thread={thread} />
</div>
)}
<div className="threads-list-item-col-title">
<a href={thread.url.index} className="threads-list-item-title">
{thread.title}
</a>
<a
href={isNew ? thread.url.new_post : thread.url.index}
className={"threads-list-item-title-sm" + (isNew ? " threads-list-item-title-new" : "")}
>
{thread.title}
</a>
</div>
{(showOptions && thread.moderation.length > 0) && (
<div className="threads-list-item-col-checkbox-sm">
<ThreadsListItemCheckbox
checked={isSelected}
disabled={isBusy}
thread={thread}
/>
</div>
)}
</div>
<div className="threads-list-item-bottom-row">
{hasFlags && (
<div className="threads-list-item-col-flags">
<ThreadsListItemFlags thread={thread} />
</div>
)}
{!!category && (
<div className="threads-list-item-col-category">
<ThreadsListItemCategory parent={parent} category={category} />
</div>
)}
<div className="threads-list-item-col-replies">
<ThreadsListItemReplies thread={thread} />
</div>
<div className="threads-list-item-col-last-poster">
<ThreadsListItemLastPoster thread={thread} />
</div>
<div className="threads-list-item-col-last-activity">
<ThreadsListItemActivity thread={thread} />
</div>
{showOptions && showSubscription && (
<div className="threads-list-item-col-subscription">
<ThreadsListItemSubscription
disabled={isBusy}
thread={thread}
/>
</div>
)}
{(showOptions && thread.moderation.length > 0) && (
<div className="threads-list-item-col-checkbox">
<ThreadsListItemCheckbox
checked={isSelected}
disabled={isBusy}
thread={thread}
/>
</div>
)}
</div>
</li>
)
}

export default ThreadsListItem
19 changes: 19 additions & 0 deletions frontend/src/components/ThreadsList/ThreadsListItemActivity.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react"

const ThreadsListItemActivity = ({ thread }) => (
<a
href={thread.url.last_post}
className="threads-list-item-last-activity"
title={interpolate(
gettext("Last activity: %(timestamp)s"),
{
timestamp: thread.last_post_on.format("LLL"),
},
true
)}
>
{thread.last_post_on.fromNow(true)}
</a>
)

export default ThreadsListItemActivity
36 changes: 36 additions & 0 deletions frontend/src/components/ThreadsList/ThreadsListItemCategory.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react"

const ThreadsListItemCategory = ({ parent, category }) => (
<span>
{parent && (
<a
href={parent.url.index}
className={getClassName(parent) + " threads-list-item-parent-category"}
style={parent.color ? {"--label-color": parent.color} : null}
title={!!parent.short_name ? parent.name : null}
>
{parent.short_name || parent.name}
</a>
)}
<a
href={category.url.index}
className={getClassName(category)}
style={category.color ? {"--label-color": category.color} : null}
title={!!category.short_name ? category.name : null}
>
{category.short_name || category.name}
</a>
</span>
)

const getClassName = (category) => {
let className = "threads-list-item-category threads-list-category-label"

if (category.color) {
className += " threads-list-category-label-color"
}

return className
}

export default ThreadsListItemCategory
18 changes: 18 additions & 0 deletions frontend/src/components/ThreadsList/ThreadsListItemCheckbox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react"
import * as select from "../../reducers/selection"
import store from "../../services/store"

const ThreadsListItemCheckbox = ({ checked, disabled, thread }) => (
<button
className="btn btn-default btn-icon"
type="button"
disabled={disabled}
onClick={() => store.dispatch(select.item(thread.id))}
>
<span className="material-icon">
{checked ? "check_box" : "check_box_outline_blank"}
</span>
</button>
)

export default ThreadsListItemCheckbox
64 changes: 64 additions & 0 deletions frontend/src/components/ThreadsList/ThreadsListItemFlags.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react"

const ThreadsListItemFlags = ({ thread }) => (
<ul className="threads-list-item-flags">
{thread.weight == 2 && (
<li
className="threads-list-item-flag-pinned-globally"
title={gettext("Pinned globally")}
>
<span className="material-icon">bookmark</span>
</li>
)}
{thread.weight == 1 && (
<li
className="threads-list-item-flag-pinned-locally"
title={gettext("Pinned in category")}
>
<span className="material-icon">bookmark_outline</span>
</li>
)}
{thread.best_answer && (
<li
className="threads-list-item-flag-answered"
title={gettext("Answered")}
>
<span className="material-icon">check_circle</span>
</li>
)}
{thread.has_poll && (
<li
className="threads-list-item-flag-poll"
title={gettext("Poll")}
>
<span className="material-icon">poll</span>
</li>
)}
{(thread.is_unapproved || thread.has_unapproved_posts) && (
<li
className="threads-list-item-flag-unapproved"
title={thread.is_unapproved ? gettext("Awaiting approval") : gettext("Has unapproved posts")}
>
<span className="material-icon">visibility</span>
</li>
)}
{thread.is_closed && (
<li
className="threads-list-item-flag-closed"
title={gettext("Closed")}
>
<span className="material-icon">lock</span>
</li>
)}
{thread.is_hidden && (
<li
className="threads-list-item-flag-hidden"
title={gettext("Hidden")}
>
<span className="material-icon">visibility_off</span>
</li>
)}
</ul>
)

export default ThreadsListItemFlags
Loading