summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ui/assets/css/main.css32
-rw-r--r--ui/assets/css/tippy.css1
-rw-r--r--ui/package.json3
-rw-r--r--ui/src/components/comment-form.tsx14
-rw-r--r--ui/src/components/comment-node.tsx824
-rw-r--r--ui/src/components/community.tsx18
-rw-r--r--ui/src/components/iframely-card.tsx2
-rw-r--r--ui/src/components/inbox.tsx1
-rw-r--r--ui/src/components/main.tsx38
-rw-r--r--ui/src/components/modlog.tsx2
-rw-r--r--ui/src/components/moment-time.tsx30
-rw-r--r--ui/src/components/navbar.tsx29
-rw-r--r--ui/src/components/post-form.tsx16
-rw-r--r--ui/src/components/post-listing.tsx185
-rw-r--r--ui/src/components/post-listings.tsx2
-rw-r--r--ui/src/components/post.tsx20
-rw-r--r--ui/src/components/private-message-form.tsx2
-rw-r--r--ui/src/components/sidebar.tsx30
-rw-r--r--ui/src/components/symbols.tsx78
-rw-r--r--ui/src/components/user.tsx1
-rw-r--r--ui/src/index.html1
-rw-r--r--ui/src/interfaces.ts1
-rw-r--r--ui/src/services/WebSocketService.ts15
-rw-r--r--ui/src/utils.ts34
-rw-r--r--ui/translations/en.json23
-rw-r--r--ui/yarn.lock20
26 files changed, 932 insertions, 490 deletions
diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css
index b03f2703..53237793 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: 1px 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 00000000..ff0a3132
--- /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 f12f947a..e3cabae5 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 eaa054d8..aa8e651d 100644
--- a/ui/src/components/comment-form.tsx
+++ b/ui/src/components/comment-form.tsx
@@ -141,16 +141,22 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<a
href={markdownHelpUrl}
target="_blank"
- class="d-inline-block float-right text-muted small font-weight-bold"
+ class="d-inline-block float-right text-muted font-weight-bold"
+ title={i18n.t('formatting_help')}
>
- {i18n.t('formatting_help')}
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-help-circle"></use>
+ </svg>
</a>
- <form class="d-inline-block mr-2 float-right text-muted small font-weight-bold">
+ <form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`${UserService.Instance.user && 'pointer'}`}
+ data-tippy-content={i18n.t('upload_image')}
>
- {i18n.t('upload_image')}
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-image"></use>
+ </svg>
</label>
<input
id={`file-upload-${this.id}`}
diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx
index cd95a75b..db3c589d 100644
--- a/ui/src/components/comment-node.tsx
+++ b/ui/src/components/comment-node.tsx
@@ -26,6 +26,8 @@ import {
isMod,
pictshareAvatarThumbnail,
showAvatars,
+ setupTippy,
+ colorList,
} from '../utils';
import moment from 'moment';
import { MomentTime } from './moment-time';
@@ -53,6 +55,7 @@ interface CommentNodeState {
score: number;
upvotes: number;
downvotes: number;
+ borderColor: string;
}
interface CommentNodeProps {
@@ -91,6 +94,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
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,438 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
return (
<div
className={`comment ${
- node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''
+ node.comment.parent_id && !this.props.noIndent ? 'ml-2' : ''
}`}
>
- {!this.state.collapsed && (
- <div
- className={`vote-bar mr-2 float-left small text-center ${this.props
- .viewOnly && 'no-click'}`}
- >
- <button
- className={`vote-animate btn btn-link p-0 ${
- this.state.my_vote == 1 ? 'text-info' : 'text-muted'
- }`}
- onClick={linkEvent(node, this.handleCommentUpvote)}
- >
- <svg class="icon upvote">
- <use xlinkHref="#icon-arrow-up"></use>
- </svg>
- </button>
- <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
- {WebSocketService.Instance.site.enable_downvotes && (
- <button
- className={`vote-animate btn btn-link p-0 ${
- this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
- }`}
- onClick={linkEvent(node, this.handleCommentDownvote)}
- >
- <svg class="icon downvote">
- <use xlinkHref="#icon-arrow-down"></use>
- </svg>
- </button>
- )}
- </div>
+ {!node.comment.parent_id && !this.props.noIndent && (
+ <hr class="d-sm-none my-2" />
)}
<div
id={`comment-${node.comment.id}`}
- className={`details comment-node ml-4 ${
+ className={`details comment-node mb-1 ${
this.isCommentNew ? 'mark' : ''
}`}
+ style={
+ !this.props.noIndent &&
+ this.props.node.comment.parent_id &&
+ `border-left: 1px ${this.state.borderColor} solid !important`
+ }
>
- <ul class="list-inline mb-0 text-muted small">
- <li className="list-inline-item">
- <Link
- className="text-info"
- to={`/u/${node.comment.creator_name}`}
- >
- {node.comment.creator_avatar && showAvatars() && (
- <img
- height="32"
- width="32"
- src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
- class="rounded-circle mr-1"
- />
- )}
- <span>{node.comment.creator_name}</span>
- </Link>
- </li>
- {this.isMod && (
- <li className="list-inline-item badge badge-light">
- {i18n.t('mod')}
- </li>
- )}
- {this.isAdmin && (
- <li className="list-inline-item badge badge-light">
- {i18n.t('admin')}
- </li>
- )}
- {this.isPostCreator && (
- <li className="list-inline-item badge badge-light">
- {i18n.t('creator')}
- </li>
- )}
- {(node.comment.banned_from_community || node.comment.banned) && (
- <li className="list-inline-item badge badge-danger">
- {i18n.t('banned')}
- </li>
- )}
- <li className="list-inline-item">
- <span>
- (<span className="text-info">+{this.state.upvotes}</span>
- <span> | </span>
- <span className="text-danger">-{this.state.downvotes}</span>
- <span>) </span>
- </span>
- </li>
- {this.props.showCommunity && (
+ <div
+ class={`${!this.props.noIndent &&
+ this.props.node.comment.parent_id &&
+ 'ml-2'}`}
+ >
+ <ul class="list-inline mb-1 text-muted small">
<li className="list-inline-item">
- <span> {i18n.t('to')} </span>
- <Link to={`/c/${node.comment.community_name}`}>
- {node.comment.community_name}
+ <Link
+ className="text-body font-weight-bold"
+ to={`/u/${node.comment.creator_name}`}
+ >
+ {node.comment.creator_avatar && showAvatars() && (
+ <img
+ height="32"
+ width="32"
+ src={pictshareAvatarThumbnail(
+ node.comment.creator_avatar
+ )}
+ class="rounded-circle mr-1"
+ />
+ )}
+ <span>{node.comment.creator_name}</span>
</Link>
</li>
- )}
- <li className="list-inline-item">
- <span>
- <MomentTime data={node.comment} />
- </span>
- </li>
- <li className="list-inline-item">
- <div
- className="pointer text-monospace"
- onClick={linkEvent(this, this.handleCommentCollapse)}
+ {this.isMod && (
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('mod')}
+ </li>
+ )}
+ {this.isAdmin && (
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('admin')}
+ </li>
+ )}
+ {this.isPostCreator && (
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('creator')}
+ </li>
+ )}
+ {(node.comment.banned_from_community || node.comment.banned) && (
+ <li className="list-inline-item badge badge-danger">
+ {i18n.t('banned')}
+ </li>
+ )}
+ <li className="list-inline-item">•</li>
+ <span
+ class="unselectable pointer mr-2"
+ data-tippy-content={i18n.t('number_of_points', {
+ count: this.state.score,
+ })}
>
- {this.state.collapsed ? '[+]' : '[-]'}
- </div>
- </li>
- </ul>
- {this.state.showEdit && (
- <CommentForm
- node={node}
- edit
- onReplyCancel={this.handleReplyCancel}
- disabled={this.props.locked}
- />
- )}
- {!this.state.showEdit && !this.state.collapsed && (
- <div>
- {this.state.viewSource ? (
- <pre>{this.commentUnlessRemoved}</pre>
- ) : (
- <div
- className="md-div"
- dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)}
- />
+ <li className="list-inline-item">
+ <span className={this.scoreColor}>
+ <svg className="small icon icon-inline mr-1">
+ <use xlinkHref="#icon-zap"></use>
+ </svg>
+ {this.state.score}
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <svg class="small icon icon-inline mr-1">
+ <use xlinkHref="#icon-arrow-up"></use>
+ </svg>
+ {this.state.upvotes}
+ </li>
+ <li className="list-inline-item">
+ <svg class="small icon icon-inline mr-1">
+ <use xlinkHref="#icon-arrow-down"></use>
+ </svg>
+ {this.state.downvotes}
+ </li>
+ </span>
+ {this.props.showCommunity && (
+ <li className="list-inline-item">
+ <span> {i18n.t('to')} </span>
+ <Link to={`/c/${node.comment.community_name}`}>
+ {node.comment.community_name}
+ </Link>
+ </li>
)}
- <ul class="list-inline mb-1 text-muted small font-weight-bold">
- {this.props.markable && (
- <li className="list-inline-item">
- <span
- class="pointer"
- onClick={linkEvent(this, this.handleMarkRead)}
- >
- {node.comment.read
- ? i18n.t('mark_as_unread')
- : i18n.t('mark_as_read')}
- </span>
- </li>
+ <li className="list-inline-item">•</li>
+ <li className="list-inline-item">
+ <span>
+ <MomentTime data={node.comment} />
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <div
+ className="unselectable pointer text-monospace"
+ onClick={linkEvent(this, this.handleCommentCollapse)}
+ >
+ {this.state.collapsed ? (
+ <svg class="icon">
+ <use xlinkHref="#icon-plus-square"></use>
+ </svg>
+ ) : (
+ <svg class="icon">
+ <use xlinkHref="#icon-minus-square"></use>
+ </svg>
+ )}
+ </div>
+ </li>
+ </ul>
+ {this.state.showEdit && (
+ <CommentForm
+ node={node}
+ edit
+ onReplyCancel={this.handleReplyCancel}
+ disabled={this.props.locked}
+ />
+ )}
+ {!this.state.showEdit && !this.state.collapsed && (
+ <div>
+ {this.state.viewSource ? (
+ <pre>{this.commentUnlessRemoved}</pre>
+ ) : (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(
+ this.commentUnlessRemoved
+ )}
+ />
)}
- {UserService.Instance.user && !this.props.viewOnly && (
- <>
- <li className="list-inline-item">
- <span
- class="pointer"
- onClick={linkEvent(this, this.handleReplyClick)}
- >
- {i18n.t('reply')}
- </span>
- </li>
- <li className="list-inline-item mr-2">
+ <ul class="list-inline mb-0 text-muted font-weight-bold h5">
+ {this.props.markable && (
+ <li className="list-inline-item-action">
<span
class="pointer"
- onClick={linkEvent(this, this.handleSaveCommentClick)}
+ onClick={linkEvent(this, this.handleMarkRead)}
+ data-tippy-content={
+ node.comment.read
+ ? i18n.t('mark_as_unread')
+ : i18n.t('mark_as_read')
+ }
>
- {node.comment.saved ? i18n.t('unsave') : i18n.t('save')}
+ <svg
+ class={`icon icon-inline ${node.comment.read &&
+ 'text-success'}`}
+ >
+ <use xlinkHref="#icon-check"></use>
+ </svg>
</span>
</li>
- {!this.myComment && (
- <li className="list-inline-item">
- <Link
- class="text-muted"
- to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
+ )}
+ {UserService.Instance.user && !this.props.viewOnly && (
+ <>
+ <li className="list-inline-item-action">
+ <button
+ className={`vote-animate btn btn-link p-0 mb-1 ${
+ this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+ }`}
+ onClick={linkEvent(node, this.handleCommentUpvote)}
+ data-tippy-content={i18n.t('upvote')}
>
- {i18n.t('message').toLowerCase()}
- </Link>
+ <svg class="icon">
+ <use xlinkHref="#icon-arrow-up"></use>
+ </svg>
+ </button>
</li>
- )}
- <li className="list-inline-item">
- <Link
- className="text-muted"
- to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
- >
- {i18n.t('link')}
- </Link>
- </li>
- {!this.state.showAdvanced ? (
- <li className="list-inline-item">
+ {WebSocketService.Instance.site.enable_downvotes && (
+ <li className="list-inline-item-action">
+ <button
+ className={`vote-animate btn btn-link p-0 mb-1 ${
+ this.state.my_vote == -1
+ ? 'text-danger'
+ : 'text-muted'
+ }`}
+ onClick={linkEvent(
+ node,
+ this.handleCommentDownvote
+ )}
+ data-tippy-content={i18n.t('downvote')}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-arrow-down"></use>
+ </svg>
+ </button>
+ </li>
+ )}
+ <li className="list-inline-item-action">
<span
- className="pointer"
- onClick={linkEvent(this, this.handleShowAdvanced)}
+ class="pointer"
+ onClick={linkEvent(this, this.handleReplyClick)}
+ data-tippy-content={i18n.t('reply')}
>
- {i18n.t('more')}
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-reply1"></use>
+ </svg>
</span>
</li>
- ) : (
- <>
- <li className="list-inline-item">•</li>
- <li className="list-inline-item">
+ {!this.myComment && (
+ <li className="list-inline-item-action">
+ <Link
+ class="text-muted"
+ to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
+ title={i18n.t('message').toLowerCase()}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-mail"></use>
+ </svg>
+ </Link>
+ </li>
+ )}
+ <li className="list-inline-item-action">
+ <Link
+ className="text-muted"
+ to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
+ title={i18n.t('link')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-link"></use>
+ </svg>
+ </Link>
+ </li>
+ {!this.state.showAdvanced ? (
+ <li className="list-inline-item-action">
<span
- className="pointer"
- onClick={linkEvent(this, this.handleViewSource)}
+ className="unselectable pointer"
+ onClick={linkEvent(this, this.handleShowAdvanced)}
+ data-tippy-content={i18n.t('more')}
>
- {i18n.t('view_source')}
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-more-vertical"></use>
+ </svg>
</span>
</li>
- <li className="list-inline-item">•</li>
- {this.myComment && (
- <>
- <li className="list-inline-item">
- <span
- class="pointer"
- onClick={linkEvent(this, this.handleEditClick)}
+ ) : (
+ <>
+ <li className="list-inline-item-action">
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleSaveCommentClick
+ )}
+ data-tippy-content={
+ node.comment.saved
+ ? i18n.t('unsave')
+ : i18n.t('save')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${node.comment.saved &&
+ 'text-warning'}`}
>
- {i18n.t('edit')}
- </span>
- </li>
- <li className="list-inline-item">
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleDeleteClick
- )}
+ <use xlinkHref="#icon-star"></use>
+ </svg>
+ </span>
+ </li>
+ <li className="list-inline-item-action">
+ <span
+ className="pointer"
+ onClick={linkEvent(this, this.handleViewSource)}
+ data-tippy-content={i18n.t('view_source')}
+ >
+ <svg
+ class={`icon icon-inline ${this.state
+ .viewSource && 'text-success'}`}
>
- {!node.comment.deleted
- ? i18n.t('delete')
- : i18n.t('restore')}
- </span>
- </li>
- </>
- )}
- {/* Admins and mods can remove comments */}
- {(this.canMod || this.canAdmin) && (
- <>
- <li className="list-inline-item">
- {!node.comment.removed ? (
+ <use xlinkHref="#icon-file-text"></use>
+ </svg>
+ </span>
+ </li>
+ {this.myComment && (
+ <>
+ <li className="list-inline-item-action">•</li>
+ <li className="list-inline-item-action">
<span
class="pointer"
onClick={linkEvent(
this,
- this.handleModRemoveShow
+ this.handleEditClick
)}
+ data-tippy-content={i18n.t('edit')}
>
- {i18n.t('remove')}
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-edit"></use>
+ </svg>
</span>
- ) : (
+ </li>
+ <li className="list-inline-item-action">
<span
class="pointer"
onClick={linkEvent(
this,
- this.handleModRemoveSubmit
+ this.handleDeleteClick
)}
+ data-tippy-content={
+ !node.comment.deleted
+ ? i18n.t('delete')
+ : i18n.t('restore')
+ }
>
- {i18n.t('restore')}
+ <svg
+ class={`icon icon-inline ${node.comment
+ .deleted && 'text-danger'}`}
+ >
+ <use xlinkHref="#icon-trash"></use>
+ </svg>
</span>
- )}
- </li>
- </>
- )}
- {/* Mods can ban from community, and appoint as mods to community */}
- {this.canMod && (
- <>
- {!this.isMod && (
- <li className="list-inline-item">
- {!node.comment.banned_from_community ? (
+ </li>
+ </>
+ )}
+ {/* Admins and mods can remove comments */}
+ {(this.canMod || this.canAdmin) && (
+ <>
+ <li className="list-inline-item-action">
+ {!node.comment.removed ? (
<span
class="pointer"
onClick={linkEvent(
this,
- this.handleModBanFromCommunityShow
+ this.handleModRemoveShow
)}
>
- {i18n.t('ban')}
+ {i18n.t('remove')}
</span>
) : (
<span
class="pointer"
onClick={linkEvent(
this,
- this.handleModBanFromCommunitySubmit
+ this.handleModRemoveSubmit
)}
>
- {i18n.t('unban')}
+ {i18n.t('restore')}