diff --git a/composer.json b/composer.json index e14bede..81fd7ce 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,10 @@ "name": "fas fa-thumbs-up", "backgroundColor": "#e74c3c", "color": "#fff" - } + }, + "optional-dependencies": [ + "flarum/likes" + ] }, "flagrow": { "discuss": "https://discuss.flarum.org/d/20671" diff --git a/extend.php b/extend.php index 143a1c0..ca20ab2 100644 --- a/extend.php +++ b/extend.php @@ -70,7 +70,6 @@ 'useAlternateLayout', 'upVotesOnly', 'iconNameAlt', - 'altPostVotingUi', ]), (new Extend\Routes('api')) diff --git a/js/src/admin/components/SettingsPage.js b/js/src/admin/components/SettingsPage.js old mode 100755 new mode 100644 index 574715b..b402b8c --- a/js/src/admin/components/SettingsPage.js +++ b/js/src/admin/components/SettingsPage.js @@ -31,7 +31,6 @@ export default class SettingsPage extends ExtensionPage { 'rateLimit', 'showVotesOnDiscussionPage', 'useAlternateLayout', - 'altPostVotingUi', 'upVotesOnly', 'firstPostOnly', 'allowSelfVotes', @@ -364,14 +363,6 @@ export default class SettingsPage extends ExtensionPage { 50 ); - items.add( - 'altPostLayout', - - {app.translator.trans('fof-gamification.admin.page.votes.alternate_post_layout')} - , - 40 - ); - items.add( 'upvotesOnly', diff --git a/js/src/forum/addUpvoteTabToUserProfile.js b/js/src/forum/addUpvoteTabToUserProfile.tsx similarity index 97% rename from js/src/forum/addUpvoteTabToUserProfile.js rename to js/src/forum/addUpvoteTabToUserProfile.tsx index a883ac0..8caeace 100644 --- a/js/src/forum/addUpvoteTabToUserProfile.js +++ b/js/src/forum/addUpvoteTabToUserProfile.tsx @@ -9,6 +9,9 @@ export default function addUpvoteTabToUserProfile() { app.routes['user.votes'] = { path: '/u/:username/votes', component: VotesUserPage }; extend(UserPage.prototype, 'navItems', function (items) { const user = this.user; + + if (!user) return; + const icon = setting('iconName') || 'thumbs'; items.add( 'votes', diff --git a/js/src/forum/addVoteButtons.js b/js/src/forum/addVoteButtons.js index 56550c0..d5533dc 100644 --- a/js/src/forum/addVoteButtons.js +++ b/js/src/forum/addVoteButtons.js @@ -1,75 +1,24 @@ import app from 'flarum/forum/app'; import { extend } from 'flarum/common/extend'; import Button from 'flarum/common/components/Button'; -import CommentPost from 'flarum/forum/components/CommentPost'; -import classList from 'flarum/common/utils/classList'; import PostControls from 'flarum/forum/utils/PostControls'; import VotesModal from './components/VotesModal'; -import setting from './helpers/setting'; -import saveVote from './helpers/saveVote'; export default function () { extend(PostControls, 'moderationControls', function (items, post) { - if (post.seeVoters()) { - items.add('viewVotes', [ - m( - Button, - { - icon: 'fas fa-thumbs-up', - onclick: () => { - app.modal.show(VotesModal, { post }); - }, - }, - app.translator.trans('fof-gamification.forum.mod_item') - ), - ]); + if (post.contentType() === 'comment' && post.seeVoters()) { + items.add( + 'viewVotes', + + ); } }); - - extend(CommentPost.prototype, 'actionItems', function (items) { - const post = this.attrs.post; - - //if (!post.canVote()) return; - - const hasDownvoted = post.hasDownvoted(); - const hasUpvoted = post.hasUpvoted(); - - const icon = setting('iconName') || 'thumbs'; - const upVotesOnly = setting('upVotesOnly', true); - - const canSeeVotes = post.canSeeVotes(); - - // We set canVote to true for guest users so that they can access the login by clicking the button - const canVote = !app.session.user || post.canVote(); - - const onclick = (upvoted, downvoted) => saveVote(post, upvoted, downvoted, (val) => (this.voteLoading = val)); - - items.add( - 'votes', -
- {Button.component({ - icon: this.voteLoading ? undefined : `fas fa-fw fa-${icon}-up`, - className: classList('Post-vote Post-upvote', hasUpvoted && 'Post-vote--active'), - loading: this.voteLoading, - disabled: this.voteLoading || !canVote || !canSeeVotes, - onclick: () => onclick(!hasUpvoted, false), - 'aria-label': app.translator.trans('fof-gamification.forum.post.upvote_button'), - })} - - - - {!upVotesOnly && - Button.component({ - icon: this.voteLoading ? undefined : `fas fa-fw fa-${icon}-down`, - className: classList('Post-vote Post-downvote', hasDownvoted && 'Post-vote--active'), - loading: this.voteLoading, - disabled: !canVote || !canSeeVotes, - onclick: () => onclick(false, !hasDownvoted), - 'aria-label': app.translator.trans('fof-gamification.forum.post.downvote_button'), - })} -
, - 10 - ); - }); } diff --git a/js/src/forum/addVotersToDiscussionPageSideBar.tsx b/js/src/forum/addVotersToDiscussionPageSideBar.tsx deleted file mode 100644 index dd11699..0000000 --- a/js/src/forum/addVotersToDiscussionPageSideBar.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import app from 'flarum/forum/app'; -import { extend } from 'flarum/common/extend'; -import DiscussionPage from 'flarum/forum/components/DiscussionPage'; -import Voters from './components/Voters'; -import type ItemList from 'flarum/common/utils/ItemList'; - -import type Mithril from 'mithril'; - -/** - * Adds our custom {@link Voters} component to the discussion sidebar. - */ -export default function addVotersToDiscussionPageSideBar() { - extend(DiscussionPage.prototype, 'sidebarItems', function (this: DiscussionPage, items: ItemList) { - const discussion = this.discussion; - const posts = discussion!.posts() || []; - const firstPost = posts?.[0]; - - if (firstPost?.canSeeVotes?.() && firstPost?.seeVoters?.() && !!app.forum.attribute('fof-gamification-op-votes-only')) { - items.add('op-voters', , 90); - } - }); -} diff --git a/js/src/forum/addVotersToEligiblePosts.tsx b/js/src/forum/addVotersToEligiblePosts.tsx new file mode 100644 index 0000000..aade611 --- /dev/null +++ b/js/src/forum/addVotersToEligiblePosts.tsx @@ -0,0 +1,19 @@ +import { extend } from 'flarum/common/extend'; +import type ItemList from 'flarum/common/utils/ItemList'; +import CommentPost from 'flarum/forum/components/CommentPost'; + +import type Mithril from 'mithril'; +import VotingWidget from './components/VotingWidget'; + +/** + * Adds our custom {@link VotingWidget} component to the post footer. + */ +export default function addVotersToEligiblePosts() { + extend(CommentPost.prototype, 'footerItems', function (items: ItemList) { + const post = this.attrs.post; + + if (post?.canSeeVotes() && post?.seeVoters()) { + items.add('post-voters', , -7); + } + }); +} diff --git a/js/src/forum/components/Voters.tsx b/js/src/forum/components/Voters.tsx index d4f0440..68e1a97 100644 --- a/js/src/forum/components/Voters.tsx +++ b/js/src/forum/components/Voters.tsx @@ -9,8 +9,14 @@ import icon from 'flarum/common/helpers/icon'; import SubtreeRetainer from 'flarum/common/utils/SubtreeRetainer'; import type Mithril from 'mithril'; +import Post from 'flarum/common/models/Post'; +import User from 'flarum/common/models/User'; -export default class Voters extends Component { +interface IAttrs { + post: Post; +} + +export default class Voters extends Component { subtreeRetainer!: SubtreeRetainer; lastRenderVotes: number = -1; loading: boolean = false; @@ -46,24 +52,54 @@ export default class Voters extends Component { } } + viewnew() { + const max = 15; + const votes = this.attrs.post.votes?.(); + const upvotes = this.attrs.post.upvotes?.(); + const downvotes = this.attrs.post.downvotes?.(); + const downvotesEnabled = false; + + return ( +
+
+ {icon('fas fa-users')} + {app.translator.trans('fof-gamification.forum.voters.label')} +
+
+ {votes && upvotes ? ( +
+ Upvoters + {this.buildVoters(upvotes, max)} +
+ ) : ( + + )} + {votes && downvotes && downvotesEnabled ? ( +
+ Downvoters + {this.buildVoters(upvotes, max)} +
+ ) : null} +
+
+ ); + } + view() { - // if (this.loading) { if (this.attrs.post.votes() === false || this.attrs.post.upvotes() === false) { return ( -
-
-
- - {icon('fas fa-users')} - {app.translator.trans('fof-gamification.forum.voters.label')} - - {app.translator.trans('fof-gamification.forum.voters.label')} - +
+
+ + {icon('fas fa-users')} + {app.translator.trans('fof-gamification.forum.voters.label')} + + {app.translator.trans('fof-gamification.forum.voters.label')} -
- - +
+ +
); } @@ -72,35 +108,53 @@ export default class Voters extends Component { const voters = this.attrs.post.upvotes(); return ( -
-
-
- - {icon('fas fa-users')} - {app.translator.trans('fof-gamification.forum.voters.label')} - - {voters.length === 0 - ? app.translator.trans('fof-gamification.forum.voters.label_none') - : app.translator.trans('fof-gamification.forum.voters.label')} - +
+
+ + {icon('fas fa-users')} + {app.translator.trans('fof-gamification.forum.voters.label')} + + {voters.length === 0 + ? app.translator.trans('fof-gamification.forum.voters.label_none') + : app.translator.trans('fof-gamification.forum.voters.label')} -
-
- {voters.length === 0 ? app.translator.trans('fof-gamification.forum.voters.none') : null} -
-
- {voters.slice(0, max).map((user: any) => ( - - {avatar(user)} - - ))} - {voters.length > max ? ( - - {`+${voters.length - max}`} - - ) : null} -
+ +
+
+ {voters.length === 0 ? app.translator.trans('fof-gamification.forum.voters.none') : null}
+
+ {voters.slice(0, max).map((user: any) => ( + + {avatar(user)} + + ))} + {voters.length > max ? ( + + {`+${voters.length - max}`} + + ) : null} +
+
+ ); + } + + buildVoters(voters: User[], max: number) { + if (voters.length === 0) { + return
{app.translator.trans('fof-gamification.forum.voters.none')}
; + } + return ( +
+ {voters.slice(0, max).map((user: User) => ( + + {avatar(user)} + + ))} + {voters.length > max ? ( + + {`+${voters.length - max}`} + + ) : null}
); } diff --git a/js/src/forum/components/VotingControl.tsx b/js/src/forum/components/VotingControl.tsx new file mode 100644 index 0000000..f76654c --- /dev/null +++ b/js/src/forum/components/VotingControl.tsx @@ -0,0 +1,68 @@ +import app from 'flarum/forum/app'; +import Component from 'flarum/common/Component'; +import Post from 'flarum/common/models/Post'; +import Button from 'flarum/common/components/Button'; +import abbreviateNumber from 'flarum/common/utils/abbreviateNumber'; +import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; +import setting from '../helpers/setting'; +import saveVote from '../helpers/saveVote'; +import Tooltip from 'flarum/common/components/Tooltip'; + +interface IAttrs { + post: Post; + voteLoading: boolean; +} + +export default class VotingControl extends Component { + view() { + const post = this.attrs.post; + + const hasDownvoted = post.hasDownvoted(); + const hasUpvoted = post.hasUpvoted(); + + const icon = setting('iconName') || 'thumbs'; + const upvotesOnly = setting('upVotesOnly', true); + + const canSeeVotes = post.canSeeVotes(); + + // We set canVote to true for guest users so that they can access the login by clicking the button + const canVote = !app.session.user || post.canVote(); + + const onclick = (upvoted: boolean, downvoted: boolean) => + saveVote(post, upvoted, downvoted, (val: boolean) => { + this.attrs.voteLoading = val; + }); + + return ( +
+ +
+ ); + } +} diff --git a/js/src/forum/components/VotingWidget.tsx b/js/src/forum/components/VotingWidget.tsx new file mode 100755 index 0000000..e97f9c3 --- /dev/null +++ b/js/src/forum/components/VotingWidget.tsx @@ -0,0 +1,48 @@ +import Component from 'flarum/common/Component'; +import Post from 'flarum/common/models/Post'; +import ItemList from 'flarum/common/utils/ItemList'; +import type Mithril from 'mithril'; +import Voters from './Voters'; +import VotingControl from './VotingControl'; + +interface IAttrs { + post: Post; +} + +export default class VotingWidget extends Component { + loading: boolean = false; + + view() { + return ( +
+
{this.headerItems().toArray()}
+
+
{this.voteActionItems().toArray()}
+
{this.votersItems().toArray()}
+
+
+ ); + } + + headerItems() { + const items = new ItemList(); + + return items; + } + + voteActionItems() { + const items = new ItemList(); + + items.add('vote-control', , 100); + + return items; + } + + votersItems() { + const items = new ItemList(); + + items.add('upvotes', , 100); + + return items; + } +} diff --git a/js/src/forum/components/index.js b/js/src/forum/components/index.ts similarity index 67% rename from js/src/forum/components/index.js rename to js/src/forum/components/index.ts index 23244ad..cf498b5 100644 --- a/js/src/forum/components/index.js +++ b/js/src/forum/components/index.ts @@ -2,10 +2,14 @@ import RankingsPage from './RankingsPage'; import VoteNotification from './VoteNotification'; import VotesModal from './VotesModal'; import Voters from './Voters'; +import VotingControl from './VotingControl'; +import VotingWidget from './VotingWidget'; export const components = { RankingsPage, VoteNotification, VotesModal, Voters, + VotingControl, + VotingWidget, }; diff --git a/js/src/forum/helpers/index.js b/js/src/forum/helpers/index.ts similarity index 100% rename from js/src/forum/helpers/index.js rename to js/src/forum/helpers/index.ts diff --git a/js/src/forum/index.js b/js/src/forum/index.js old mode 100755 new mode 100644 index 71df7a4..0b1f4e0 --- a/js/src/forum/index.js +++ b/js/src/forum/index.js @@ -17,9 +17,8 @@ import addAlternateLayout from './addAlternateLayout'; import setting from './helpers/setting'; import addVotesSort from './addVotesSort'; -import useAlternatePostVoteLayout from './useAlternatePostVoteLayout'; import addNotifications from './addNotifications'; -import addVotersToDiscussionPageSideBar from './addVotersToDiscussionPageSideBar'; +import addVotersToEligiblePosts from './addVotersToEligiblePosts'; import addUpvoteTabToUserProfile from './addUpvoteTabToUserProfile'; app.initializers.add('fof-gamification', () => { @@ -54,16 +53,12 @@ app.initializers.add('fof-gamification', () => { addUpvotesToDiscussion(); addPusher(); addNotifications(); - addVotersToDiscussionPageSideBar(); + addVotersToEligiblePosts(); addUpvoteTabToUserProfile(); if (setting('useAlternateLayout', true)) { addAlternateLayout(); } - - if (setting('altPostVotingUi', true)) { - useAlternatePostVoteLayout(); - } }); export * from './components'; diff --git a/js/src/forum/useAlternatePostVoteLayout.tsx b/js/src/forum/useAlternatePostVoteLayout.tsx deleted file mode 100644 index c9dd6c0..0000000 --- a/js/src/forum/useAlternatePostVoteLayout.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import app from 'flarum/forum/app'; - -import { extend } from 'flarum/common/extend'; - -import CommentPost from 'flarum/forum/components/CommentPost'; -import Button from 'flarum/common/components/Button'; -import abbreviateNumber from 'flarum/common/utils/abbreviateNumber'; -import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; -import type ItemList from 'flarum/common/utils/ItemList'; - -import setting from './helpers/setting'; -import saveVote from './helpers/saveVote'; - -export default function useAlternatePostVoteLayout() { - extend(CommentPost.prototype, 'actionItems', function (this: CommentPost, items: ItemList) { - if (this.attrs.post.isHidden()) return; - - items.remove('votes'); - }); - - extend(CommentPost.prototype, 'classes', function (this: CommentPost, classes: string[]) { - if (this.attrs.post.isHidden()) return; - - const upvotesOnly = setting('upVotesOnly', true); - - classes.push('votesAlternativeLayout'); - - if (upvotesOnly) { - classes.push('votesUpvotesOnly'); - } - }); - - extend(CommentPost.prototype, 'headerItems', function (this: CommentPost, items: ItemList) { - const post = this.attrs.post; - - if (post.isHidden()) return; - if (!post.canSeeVotes()) return; - - const hasDownvoted = post.hasDownvoted(); - const hasUpvoted = post.hasUpvoted(); - - const icon = setting('iconName') || 'thumbs'; - const upvotesOnly = setting('upVotesOnly', true); - - const canSeeVotes = post.canSeeVotes(); - - // We set canVote to true for guest users so that they can access the login by clicking the button - const canVote = !app.session.user || post.canVote(); - - const onclick = (upvoted, downvoted) => - saveVote(post, upvoted, downvoted, (val) => { - this.voteLoading = val; - }); - - items.add( - 'votes', -
-
, - 10000 - ); - }); -} diff --git a/resources/less/forum/VotesBox.less b/resources/less/forum/VotesBox.less index 8973a73..3fd4801 100644 --- a/resources/less/forum/VotesBox.less +++ b/resources/less/forum/VotesBox.less @@ -1,5 +1,5 @@ .VotingContainer { - padding: 12px; + padding: 2px; background: @control-bg; border-radius: @border-radius; @@ -36,7 +36,7 @@ } &-icon { - font-size: 0.9rem; + font-size: 0.7rem; vertical-align: middle; } @@ -68,7 +68,8 @@ } &-list { - display: grid; + display: inline-flex; + flex-wrap: wrap; grid-template-columns: repeat(auto-fill, minmax(16px, 1fr)); gap: 4px; text-align: center; diff --git a/resources/less/forum/VotingWidget.less b/resources/less/forum/VotingWidget.less new file mode 100755 index 0000000..780ea17 --- /dev/null +++ b/resources/less/forum/VotingWidget.less @@ -0,0 +1,114 @@ +.VotingWidgetContainer { + padding: 12px; + background: @control-bg; + border-radius: @border-radius; + display: flex; + + @media @phone { + padding: 8px; + } + + .VotingWidget-voters { + display: inline-flex; + + .VotingWidget-voters-list { + padding: 0 20px; + display: inline-flex; + + .Voters-info { + .Voters-info--sections { + display: grid; + column-gap: 50px; + grid-template-columns: auto auto; + } + } + } + } +} + +.VotingControl { + display: grid; + grid-template-columns: 1fr; + + cursor: pointer; + + height: 42px; + width: 36px; + + left: 0; + + background: @body-bg; + + border-radius: 4px; + border: 1px solid fade(@primary-color, 80%); + + line-height: 24px; + + .LoadingIndicator-container { + color: @control-color; + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + button { + position: relative; + display: block; + + width: 100%; + + &[data-active] { + color: @primary-color !important; + } + + &:hover, + &:focus, + &:active { + background: var(--vote-box-hover-color) !important; + } + + .icon { + position: absolute; + top: 4px; + left: 50%; + margin-right: 0; + transform: translateX(-50%); + + font-size: 1.2em !important; + } + } + + &[data-upvotes-only] { + button { + height: 100%; + } + + grid-template-rows: 1fr auto; + + .DiscussionListItem-voteCount, + .Post-voteCount { + bottom: 0; + width: 100%; + + pointer-events: none; + text-align: center; + + display: block; + height: 24px; + } + } + + &:not([data-upvotes-only]) { + height: 56px; + + grid-template-rows: 1fr auto 1fr; + + line-height: 1; + + span { + text-align: center; + } + } +} \ No newline at end of file diff --git a/resources/less/forum/extension.less b/resources/less/forum/extension.less old mode 100755 new mode 100644 index 3363f27..7b3a68c --- a/resources/less/forum/extension.less +++ b/resources/less/forum/extension.less @@ -1,6 +1,7 @@ @import "../lib/rankLabel.less"; -@import "./alternateLayout.less"; +//@import "./alternateLayout.less"; @import "./VotesBox.less"; +@import "./VotingWidget.less"; @media (min-width: 768px) { .DiscussionListItem-info .item-discussion-votes + .item-tags { @@ -8,6 +9,10 @@ } } +.Post-footer { + width: 100%; +} + .item-discussion-votes { margin-left: 5px; position: absolute; diff --git a/resources/locale/en.yml b/resources/locale/en.yml old mode 100755 new mode 100644 index 48f86f5..c7f3ec8 --- a/resources/locale/en.yml +++ b/resources/locale/en.yml @@ -35,7 +35,9 @@ fof-gamification: label_none: No voters post: upvote_button: Upvote post + upvote_tooltip: Upvote downvote_button: Downvote post + downvote_tooltip: Downvote admin: permissions: @@ -66,7 +68,6 @@ fof-gamification: rate_limit: Enforce a vote rate limit (10 seconds) discussion_page: Show total votes of original post on discussions list alternate_layout: Use alternate voting layout on discussions list - alternate_post_layout: Use alternate voting layout on posts title: Votes vote_color: Voted color icon_name: Upvote/downvote icon