diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index b03f270..b458a9d 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -74,10 +74,6 @@ border-top: 2px solid var(--dark); } -.comment-node { - margin-bottom: 10px; -} - .vote-bar { margin-top: -6.5px; } @@ -95,8 +91,17 @@ fill: currentColor; vertical-align: middle; align-self: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } +.icon-inline { + margin-bottom: 2px; +} .spin { animation: spins 2s linear infinite; @@ -112,7 +117,7 @@ } blockquote { - border-left: 3px solid #ccc; + border-left: 2px solid var(--secondary); margin: 0.5em 5px; padding: 0.1em 5px; } @@ -225,3 +230,20 @@ hr { height: 50px; width: 50px; } + +.unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.list-inline-item-action { + display: inline-block; +} + +.list-inline-item-action:not(:last-child) { + margin-right: 1.2rem; +} diff --git a/ui/assets/css/tippy.css b/ui/assets/css/tippy.css new file mode 100644 index 0000000..ff0a313 --- /dev/null +++ b/ui/assets/css/tippy.css @@ -0,0 +1 @@ +.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}.tippy-iOS{cursor:pointer!important;-webkit-tap-highlight-color:transparent}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-width:8px 8px 0;border-top-color:#333;bottom:-7px;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;border-width:0 8px 8px;border-bottom-color:#333;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:#333;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:#333;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index f12f947..e3cabae 100644 --- a/ui/package.json +++ b/ui/package.json @@ -41,8 +41,9 @@ "reconnecting-websocket": "^4.4.0", "rxjs": "^6.4.0", "terser": "^4.6.3", + "tippy.js": "^6.0.0", "toastify-js": "^1.6.2", - "tributejs": "^4.1.1", + "tributejs": "^5.0.0", "twemoji": "^12.1.2", "ws": "^7.0.0" }, diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index eaa054d..aa8e651 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -141,16 +141,22 @@ export class CommentForm extends Component { - {i18n.t('formatting_help')} + + + -
+ { score: this.props.node.comment.score, upvotes: this.props.node.comment.upvotes, downvotes: this.props.node.comment.downvotes, + borderColor: this.props.node.comment.depth + ? colorList[this.props.node.comment.depth % colorList.length] + : colorList[0], }; constructor(props: any, context: any) { @@ -115,294 +121,434 @@ export class CommentNode extends Component { return (
- {!this.state.collapsed && ( -
- -
{this.state.score}
- {WebSocketService.Instance.site.enable_downvotes && ( - - )} -
+ {!node.comment.parent_id && !this.props.noIndent && ( + <> +
+
+ )}
-
    -
  • - - {node.comment.creator_avatar && showAvatars() && ( - - )} - {node.comment.creator_name} - -
  • - {this.isMod && ( -
  • - {i18n.t('mod')} -
  • - )} - {this.isAdmin && ( -
  • - {i18n.t('admin')} -
  • - )} - {this.isPostCreator && ( -
  • - {i18n.t('creator')} -
  • - )} - {(node.comment.banned_from_community || node.comment.banned) && ( -
  • - {i18n.t('banned')} -
  • - )} -
  • - - (+{this.state.upvotes} - | - -{this.state.downvotes} - ) - -
  • - {this.props.showCommunity && ( +
    +
    • - {i18n.t('to')} - - {node.comment.community_name} + + {node.comment.creator_avatar && showAvatars() && ( + + )} + {node.comment.creator_name}
    • - )} -
    • - - - -
    • -
    • -
      - {this.state.collapsed ? '[+]' : '[-]'} -
      -
    • -
    - {this.state.showEdit && ( - - )} - {!this.state.showEdit && !this.state.collapsed && ( -
    - {this.state.viewSource ? ( -
    {this.commentUnlessRemoved}
    - ) : ( -
    + {this.isMod && ( +
  • + {i18n.t('mod')} +
  • )} -
      - {this.props.markable && ( -
    • - - {node.comment.read - ? i18n.t('mark_as_unread') - : i18n.t('mark_as_read')} - -
    • - )} - {UserService.Instance.user && !this.props.viewOnly && ( - <> -
    • - - {i18n.t('reply')} - -
    • -
    • - - {node.comment.saved ? i18n.t('unsave') : i18n.t('save')} - -
    • - {!this.myComment && ( -
    • - - {i18n.t('message').toLowerCase()} - -
    • + {this.isAdmin && ( +
    • + {i18n.t('admin')} +
    • + )} + {this.isPostCreator && ( +
    • + {i18n.t('creator')} +
    • + )} + {(node.comment.banned_from_community || node.comment.banned) && ( +
    • + {i18n.t('banned')} +
    • + )} + {this.props.showCommunity && ( +
    • + {i18n.t('to')} + + {node.comment.community_name} + +
    • + )} +
    • +
    • + + + + + {this.state.score} + +
    • +
    • +
    • + + + +
    • +
    • +
      + {this.state.collapsed ? ( + + + + ) : ( + + + + )} +
      +
    • +
    + {this.state.showEdit && ( + + )} + {!this.state.showEdit && !this.state.collapsed && ( +
    + {this.state.viewSource ? ( +
    {this.commentUnlessRemoved}
    + ) : ( +
    - + )} +
      + {this.props.markable && ( +
    • + - {i18n.t('link')} - -
    • - {!this.state.showAdvanced ? ( -
    • - - {i18n.t('more')} + + + +
    • + )} + {UserService.Instance.user && !this.props.viewOnly && ( + <> +
    • + +
    • + {WebSocketService.Instance.site.enable_downvotes && ( +
    • + +
    • + )} +
    • + + + +
    • - ) : ( - <> -
    • -
    • +
    • + + + + + +
    • + {!this.state.showAdvanced ? ( +
    • - {i18n.t('view_source')} + + +
    • -
    • - {this.myComment && ( - <> -
    • - + {!this.myComment && ( +
    • + - {i18n.t('edit')} - + + + +
    • -
    • - - {!node.comment.deleted - ? i18n.t('delete') - : i18n.t('restore')} - -
    • - - )} - {/* Admins and mods can remove comments */} - {(this.canMod || this.canAdmin) && ( - <> -
    • - {!node.comment.removed ? ( - - {i18n.t('remove')} - - ) : ( - - {i18n.t('restore')} - + )} +
    • + - - )} - {/* Mods can ban from community, and appoint as mods to community */} - {this.canMod && ( - <> - {!this.isMod && ( -
    • - {!node.comment.banned_from_community ? ( + data-tippy-content={ + node.comment.saved + ? i18n.t('unsave') + : i18n.t('save') + } + > + + + + +
    • +
    • + + + + + +
    • + {this.myComment && ( + <> +
    • +
    • + + + + + +
    • +
    • + + + + + +
    • + + )} + {/* Admins and mods can remove comments */} + {(this.canMod || this.canAdmin) && ( + <> +
    • + {!node.comment.removed ? ( - {i18n.t('ban')} + {i18n.t('remove')} ) : ( - {i18n.t('unban')} + {i18n.t('restore')} )}
    • - )} - {!node.comment.banned_from_community && ( -
    • - {!this.state.showConfirmAppointAsMod ? ( + + )} + {/* Mods can ban from community, and appoint as mods to community */} + {this.canMod && ( + <> + {!this.isMod && ( +
    • + {!node.comment.banned_from_community ? ( + + {i18n.t('ban')} + + ) : ( + + {i18n.t('unban')} + + )} +
    • + )} + {!node.comment.banned_from_community && ( +
    • + {!this.state.showConfirmAppointAsMod ? ( + + {this.isMod + ? i18n.t('remove_as_mod') + : i18n.t('appoint_as_mod')} + + ) : ( + <> + + {i18n.t('are_you_sure')} + + + {i18n.t('yes')} + + + {i18n.t('no')} + + + )} +
    • + )} + + )} + {/* Community creators and admins can transfer community to another mod */} + {(this.amCommunityCreator || this.canAdmin) && + this.isMod && ( +
    • + {!this.state.showConfirmTransferCommunity ? ( - {this.isMod - ? i18n.t('remove_as_mod') - : i18n.t('appoint_as_mod')} + {i18n.t('transfer_community')} ) : ( <> @@ -413,7 +559,7 @@ export class CommentNode extends Component { class="pointer d-inline-block mr-1" onClick={linkEvent( this, - this.handleAddModToCommunity + this.handleTransferCommunity )} > {i18n.t('yes')} @@ -422,7 +568,8 @@ export class CommentNode extends Component { class="pointer d-inline-block" onClick={linkEvent( this, - this.handleCancelConfirmAppointAsMod + this + .handleCancelShowConfirmTransferCommunity )} > {i18n.t('no')} @@ -431,21 +578,89 @@ export class CommentNode extends Component { )}
    • )} - - )} - {/* Community creators and admins can transfer community to another mod */} - {(this.amCommunityCreator || this.canAdmin) && - this.isMod && ( -
    • - {!this.state.showConfirmTransferCommunity ? ( + {/* Admins can ban from all, and appoint other admins */} + {this.canAdmin && ( + <> + {!this.isAdmin && ( +
    • + {!node.comment.banned ? ( + + {i18n.t('ban_from_site')} + + ) : ( + + {i18n.t('unban_from_site')} + + )} +
    • + )} + {!node.comment.banned && ( +
    • + {!this.state.showConfirmAppointAsAdmin ? ( + + {this.isAdmin + ? i18n.t('remove_as_admin') + : i18n.t('appoint_as_admin')} + + ) : ( + <> + + {i18n.t('are_you_sure')} + + + {i18n.t('yes')} + + + {i18n.t('no')} + + + )} +
    • + )} + + )} + {/* Site Creator can transfer to another admin */} + {this.amSiteCreator && this.isAdmin && ( +
    • + {!this.state.showConfirmTransferSite ? ( - {i18n.t('transfer_community')} + {i18n.t('transfer_site')} ) : ( <> @@ -456,7 +671,7 @@ export class CommentNode extends Component { class="pointer d-inline-block mr-1" onClick={linkEvent( this, - this.handleTransferCommunity + this.handleTransferSite )} > {i18n.t('yes')} @@ -465,8 +680,7 @@ export class CommentNode extends Component { class="pointer d-inline-block" onClick={linkEvent( this, - this - .handleCancelShowConfirmTransferCommunity + this.handleCancelShowConfirmTransferSite )} > {i18n.t('no')} @@ -475,125 +689,16 @@ export class CommentNode extends Component { )}
    • )} - {/* Admins can ban from all, and appoint other admins */} - {this.canAdmin && ( - <> - {!this.isAdmin && ( -
    • - {!node.comment.banned ? ( - - {i18n.t('ban_from_site')} - - ) : ( - - {i18n.t('unban_from_site')} - - )} -
    • - )} - {!node.comment.banned && ( -
    • - {!this.state.showConfirmAppointAsAdmin ? ( - - {this.isAdmin - ? i18n.t('remove_as_admin') - : i18n.t('appoint_as_admin')} - - ) : ( - <> - - {i18n.t('are_you_sure')} - - - {i18n.t('yes')} - - - {i18n.t('no')} - - - )} -
    • - )} - - )} - {/* Site Creator can transfer to another admin */} - {this.amSiteCreator && this.isAdmin && ( -
    • - {!this.state.showConfirmTransferSite ? ( - - {i18n.t('transfer_site')} - - ) : ( - <> - - {i18n.t('are_you_sure')} - - - {i18n.t('yes')} - - - {i18n.t('no')} - - - )} -
    • - )} - - )} - - )} -
    -
    - )} + + )} + + )} +
+
+ )} +
+ {/* end of details */} {this.state.showRemoveDialog && ( { handleShowAdvanced(i: CommentNode) { i.state.showAdvanced = !i.state.showAdvanced; i.setState(i.state); + setupTippy(); + } + + get scoreColor() { + if (this.state.my_vote == 1) { + return 'text-info'; + } else if (this.state.my_vote == -1) { + return 'text-danger'; + } else { + return 'text-muted'; + } } } diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 6738646..4e8e9d1 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -43,6 +43,7 @@ import { createPostLikeFindRes, editPostFindRes, commentsToFlatNodes, + setupTippy, } from '../utils'; import { i18n } from '../i18next'; @@ -194,13 +195,14 @@ export class Community extends Component { selects() { return ( -
- - - +
+ + + + { SortType[this.state.sort] }`} target="_blank" + title="RSS" > # @@ -339,6 +342,7 @@ export class Community extends Component { this.state.posts = data.posts; this.state.loading = false; this.setState(this.state); + setupTippy(); } else if (res.op == UserOperation.EditPost) { let data = res.data as PostResponse; editPostFindRes(data, this.state.posts); diff --git a/ui/src/components/iframely-card.tsx b/ui/src/components/iframely-card.tsx index 4bae06d..31929ea 100644 --- a/ui/src/components/iframely-card.tsx +++ b/ui/src/components/iframely-card.tsx @@ -1,6 +1,7 @@ import { Component, linkEvent } from 'inferno'; import { FramelyData } from '../interfaces'; import { mdToHtml } from '../utils'; +import { i18n } from '../i18next'; interface FramelyCardProps { iframely: FramelyData; @@ -54,6 +55,7 @@ export class IFramelyCard extends Component< {this.state.expanded ? '[-]' : '[+]'} diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 56bf157..0d07dca 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -116,6 +116,7 @@ export class Inbox extends Component { # diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 8161200..b772bd8 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -52,6 +52,7 @@ import { editPostFindRes, commentsToFlatNodes, commentSortSortType, + setupTippy, } from '../utils'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -183,7 +184,7 @@ export class Main extends Component {
# - + # @@ -221,7 +222,7 @@ export class Main extends Component {
# - + # @@ -268,13 +269,16 @@ export class Main extends Component {
{`${this.state.siteRes.site.name}`}
{this.canAdmin && ( -
    -
  • +
      +
    • - {i18n.t('edit')} + + +
    @@ -313,7 +317,10 @@ export class Main extends Component {
  • {i18n.t('admins')}:
  • {this.state.siteRes.admins.map(admin => (
  • - + {admin.avatar && showAvatars() && ( { selects() { return (
    - - + + + + { - + # @@ -454,8 +464,9 @@ export class Main extends Component { SortType[this.state.sort] }`} target="_blank" + title="RSS" > - + # @@ -613,6 +624,7 @@ export class Main extends Component { this.state.posts = data.posts; this.state.loading = false; this.setState(this.state); + setupTippy(); } else if (res.op == UserOperation.CreatePost) { let data = res.data as PostResponse; diff --git a/ui/src/components/modlog.tsx b/ui/src/components/modlog.tsx index f57e1c6..04628eb 100644 --- a/ui/src/components/modlog.tsx +++ b/ui/src/components/modlog.tsx @@ -354,7 +354,7 @@ export class Modlog extends Component {
    {this.state.communityName && ( /c/{this.state.communityName}{' '} diff --git a/ui/src/components/moment-time.tsx b/ui/src/components/moment-time.tsx index fd2a7ef..24ab2d8 100644 --- a/ui/src/components/moment-time.tsx +++ b/ui/src/components/moment-time.tsx @@ -1,6 +1,6 @@ import { Component } from 'inferno'; import moment from 'moment'; -import { getMomentLanguage } from '../utils'; +import { getMomentLanguage, capitalizeFirstLetter } from '../utils'; import { i18n } from '../i18next'; interface MomentTimeProps { @@ -23,13 +23,35 @@ export class MomentTime extends Component { render() { if (this.props.data.updated) { return ( - - {i18n.t('modified')} {moment.utc(this.props.data.updated).fromNow()} + + + + + {moment.utc(this.props.data.updated).fromNow(true)} ); } else { let str = this.props.data.published || this.props.data.when_; - return {moment.utc(str).fromNow()}; + return ( + + {moment.utc(str).fromNow(true)} + + ); } } + + format(input: string): string { + return moment + .utc(input) + .local() + .format('LLLL'); + } } diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index 75cdd55..ef3f843 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -105,6 +105,7 @@ export class Navbar extends Component { type="button" aria-label="menu" onClick={linkEvent(this, this.expandNavbar)} + data-tippy-content={i18n.t('expand_here')} > @@ -113,12 +114,16 @@ export class Navbar extends Component { >
diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 732664e..2d09e90 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -30,6 +30,7 @@ import { pictshareAvatarThumbnail, showAvatars, imageThumbnailer, + setupTippy, } from '../utils'; import { i18n } from '../i18next'; @@ -185,7 +186,7 @@ export class PostListing extends Component { return ( {this.imgThumb()} @@ -246,12 +247,18 @@ export class PostListing extends Component { this.state.my_vote == 1 ? 'text-info' : 'text-muted' }`} onClick={linkEvent(this, this.handlePostLike)} + data-tippy-content={i18n.t('upvote')} > -
+
{this.state.score}
{WebSocketService.Instance.site.enable_downvotes && ( @@ -260,6 +267,7 @@ export class PostListing extends Component { this.state.my_vote == -1 ? 'text-danger' : 'text-muted' }`} onClick={linkEvent(this, this.handlePostDisLike)} + data-tippy-content={i18n.t('downvote')} > @@ -323,7 +331,7 @@ export class PostListing extends Component { title={this.state.url} > {new URL(this.state.url).hostname} - + @@ -333,19 +341,23 @@ export class PostListing extends Component { <> {!this.state.imageExpanded ? ( - [+] + + + ) : ( - [-] + + +
{ )} {post.deleted && ( - - {i18n.t('deleted')} + + + + )} {post.locked && ( - - {i18n.t('locked')} + + + + )} {post.stickied && ( - - {i18n.t('stickied')} + + + + )} {post.nsfw && ( @@ -398,7 +425,10 @@ export class PostListing extends Component {
  • {i18n.t('by')} - + {post.creator_avatar && showAvatars() && ( { )}
  • +
  • +
  • + {this.state.upvotes !== this.state.score && ( + <> +
  • + + + + + {this.state.upvotes} + +
  • +
  • + + + + + {this.state.downvotes} + +
  • +
  • + + )}
  • - - (+{this.state.upvotes} - | - -{this.state.downvotes} - ) - -
  • -
  • - - {i18n.t('number_of_comments', { + + + + + {post.number_of_comments}
-
    +
      {this.props.post.duplicates && ( <>
    • @@ -470,93 +522,140 @@ export class PostListing extends Component { )}
    -
      +
        {UserService.Instance.user && ( <> {this.props.showBody && ( <> -
      • +
      • - {post.saved ? i18n.t('unsave') : i18n.t('save')} + + +
      • -
      • +
      • - {i18n.t('cross_post')} + + +
      • )} {this.myPost && this.props.showBody && ( <> -
      • +
      • - {i18n.t('edit')} + + +
      • -
      • +
      • - {!post.deleted - ? i18n.t('delete') - : i18n.t('restore')} + + +
      • )} {!this.state.showAdvanced && this.props.showBody ? ( -
      • +
      • - {i18n.t('more')} + + +
      • ) : ( <> {this.props.showBody && post.body && ( -
      • +
      • - {i18n.t('view_source')} + + +
      • )} {this.canModOnSelf && ( <> -
      • +
      • - {post.locked - ? i18n.t('unlock') - : i18n.t('lock')} + + +
      • -
      • +
      • - {post.stickied - ? i18n.t('unsticky') - : i18n.t('sticky')} + + +
      • @@ -1236,5 +1335,6 @@ export class PostListing extends Component { handleShowAdvanced(i: PostListing) { i.state.showAdvanced = !i.state.showAdvanced; i.setState(i.state); + setupTippy(); } } diff --git a/ui/src/components/post-listings.tsx b/ui/src/components/post-listings.tsx index d61f624..5e6acc0 100644 --- a/ui/src/components/post-listings.tsx +++ b/ui/src/components/post-listings.tsx @@ -53,7 +53,7 @@ export class PostListings extends Component { } if (this.props.sort !== undefined) { - postSort(out, this.props.sort); + postSort(out, this.props.sort, this.props.showCommunity == undefined); } return out; diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index d8f662c..ed1ba30 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -37,6 +37,7 @@ import { createCommentLikeRes, createPostLikeRes, commentsToFlatNodes, + setupTippy, } from '../utils'; import { PostListing } from './post-listing'; import { PostListings } from './post-listings'; @@ -210,7 +211,7 @@ export class Post extends Component { sortRadios() { return ( -
        +