Merge branch 'dev'
This commit is contained in:
commit
12193d418b
11 changed files with 278 additions and 93 deletions
2
docs/src/about_goals.md
vendored
2
docs/src/about_goals.md
vendored
|
@ -47,3 +47,5 @@
|
|||
- https://docs.rs/activitypub/0.1.4/activitypub/
|
||||
- [Activitypub vocab.](https://www.w3.org/TR/activitystreams-vocabulary/)
|
||||
- [Activitypub main](https://www.w3.org/TR/activitypub/)
|
||||
- [Federation.md](https://github.com/dariusk/gathio/blob/7fc93dbe9d4d99457a0e85c6c532112f415b7af2/FEDERATION.md)
|
||||
- [Activitypub implementers guide](https://socialhub.activitypub.rocks/t/draft-guide-for-new-activitypub-implementers/479)
|
||||
|
|
1
ui/.eslintrc.json
vendored
1
ui/.eslintrc.json
vendored
|
@ -38,6 +38,7 @@
|
|||
"inferno/no-direct-mutation-state": 0,
|
||||
"inferno/no-unknown-property": 0,
|
||||
"max-statements": 0,
|
||||
"max-params": 0,
|
||||
"new-cap": 0,
|
||||
"no-console": 0,
|
||||
"no-duplicate-imports": 0,
|
||||
|
|
18
ui/src/components/comment-node.tsx
vendored
18
ui/src/components/comment-node.tsx
vendored
|
@ -66,6 +66,7 @@ interface CommentNodeProps {
|
|||
viewOnly?: boolean;
|
||||
locked?: boolean;
|
||||
markable?: boolean;
|
||||
showContext?: boolean;
|
||||
moderators: Array<CommunityUser>;
|
||||
admins: Array<UserView>;
|
||||
// TODO is this necessary, can't I get it from the node itself?
|
||||
|
@ -166,17 +167,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</Link>
|
||||
</li>
|
||||
{this.isMod && (
|
||||
<li className="list-inline-item badge badge-light">
|
||||
<li className="list-inline-item badge badge-light d-none d-sm-inline">
|
||||
{i18n.t('mod')}
|
||||
</li>
|
||||
)}
|
||||
{this.isAdmin && (
|
||||
<li className="list-inline-item badge badge-light">
|
||||
<li className="list-inline-item badge badge-light d-none d-sm-inline">
|
||||
{i18n.t('admin')}
|
||||
</li>
|
||||
)}
|
||||
{this.isPostCreator && (
|
||||
<li className="list-inline-item badge badge-light">
|
||||
<li className="list-inline-item badge badge-light d-none d-sm-inline">
|
||||
{i18n.t('creator')}
|
||||
</li>
|
||||
)}
|
||||
|
@ -209,8 +210,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<li className="ml-3 list-inline-item">
|
||||
<span
|
||||
className={`unselectable pointer ${this.scoreColor}`}
|
||||
onClick={linkEvent(node, this.handleCommentUpvote)}
|
||||
|
@ -250,6 +250,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
/>
|
||||
)}
|
||||
<ul class="list-inline mb-0 text-muted font-weight-bold small">
|
||||
{this.props.showContext && this.linkBtn}
|
||||
{this.props.markable && (
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
|
@ -348,7 +349,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
{this.props.markable && this.linkBtn}
|
||||
{!this.state.showAdvanced ? (
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
|
@ -376,7 +376,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{!this.props.markable && this.linkBtn}
|
||||
{!this.props.showContext && this.linkBtn}
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
className="btn btn-link btn-sm btn-animate text-muted"
|
||||
|
@ -765,7 +765,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
<Link
|
||||
className="btn btn-link btn-sm btn-animate text-muted"
|
||||
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
|
||||
title={i18n.t('link')}
|
||||
title={
|
||||
this.props.showContext ? i18n.t('show_context') : i18n.t('link')
|
||||
}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-link"></use>
|
||||
|
|
2
ui/src/components/comment-nodes.tsx
vendored
2
ui/src/components/comment-nodes.tsx
vendored
|
@ -20,6 +20,7 @@ interface CommentNodesProps {
|
|||
viewOnly?: boolean;
|
||||
locked?: boolean;
|
||||
markable?: boolean;
|
||||
showContext?: boolean;
|
||||
showCommunity?: boolean;
|
||||
sort?: CommentSortType;
|
||||
sortType?: SortType;
|
||||
|
@ -47,6 +48,7 @@ export class CommentNodes extends Component<
|
|||
admins={this.props.admins}
|
||||
postCreatorId={this.props.postCreatorId}
|
||||
markable={this.props.markable}
|
||||
showContext={this.props.showContext}
|
||||
showCommunity={this.props.showCommunity}
|
||||
sort={this.props.sort}
|
||||
sortType={this.props.sortType}
|
||||
|
|
162
ui/src/components/inbox.tsx
vendored
162
ui/src/components/inbox.tsx
vendored
|
@ -1,5 +1,4 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import {
|
||||
|
@ -34,14 +33,13 @@ import { CommentNodes } from './comment-nodes';
|
|||
import { PrivateMessage } from './private-message';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
enum UnreadOrAll {
|
||||
Unread,
|
||||
All,
|
||||
}
|
||||
|
||||
enum UnreadType {
|
||||
enum MessageType {
|
||||
All,
|
||||
Replies,
|
||||
Mentions,
|
||||
|
@ -52,7 +50,7 @@ type ReplyType = Comment | PrivateMessageI;
|
|||
|
||||
interface InboxState {
|
||||
unreadOrAll: UnreadOrAll;
|
||||
unreadType: UnreadType;
|
||||
messageType: MessageType;
|
||||
replies: Array<Comment>;
|
||||
mentions: Array<Comment>;
|
||||
messages: Array<PrivateMessageI>;
|
||||
|
@ -64,7 +62,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
private subscription: Subscription;
|
||||
private emptyState: InboxState = {
|
||||
unreadOrAll: UnreadOrAll.Unread,
|
||||
unreadType: UnreadType.All,
|
||||
messageType: MessageType.All,
|
||||
replies: [],
|
||||
mentions: [],
|
||||
messages: [],
|
||||
|
@ -100,26 +98,19 @@ export class Inbox extends Component<any, InboxState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
let user = UserService.Instance.user;
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-0">
|
||||
<T
|
||||
class="d-inline"
|
||||
i18nKey="inbox_for"
|
||||
interpolation={{ user: user.username }}
|
||||
>
|
||||
#<Link to={`/u/${user.username}`}>#</Link>
|
||||
</T>
|
||||
<h5 class="mb-1">
|
||||
{i18n.t('inbox')}
|
||||
<small>
|
||||
<a
|
||||
href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
|
||||
target="_blank"
|
||||
title="RSS"
|
||||
>
|
||||
<svg class="icon mx-2 text-muted small">
|
||||
<svg class="icon ml-2 text-muted small">
|
||||
<use xlinkHref="#icon-rss">#</use>
|
||||
</svg>
|
||||
</a>
|
||||
|
@ -139,10 +130,10 @@ export class Inbox extends Component<any, InboxState> {
|
|||
</ul>
|
||||
)}
|
||||
{this.selects()}
|
||||
{this.state.unreadType == UnreadType.All && this.all()}
|
||||
{this.state.unreadType == UnreadType.Replies && this.replies()}
|
||||
{this.state.unreadType == UnreadType.Mentions && this.mentions()}
|
||||
{this.state.unreadType == UnreadType.Messages && this.messages()}
|
||||
{this.state.messageType == MessageType.All && this.all()}
|
||||
{this.state.messageType == MessageType.Replies && this.replies()}
|
||||
{this.state.messageType == MessageType.Mentions && this.mentions()}
|
||||
{this.state.messageType == MessageType.Messages && this.messages()}
|
||||
{this.paginator()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -150,29 +141,103 @@ export class Inbox extends Component<any, InboxState> {
|
|||
);
|
||||
}
|
||||
|
||||
unreadOrAllRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer
|
||||
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={UnreadOrAll.Unread}
|
||||
checked={this.state.unreadOrAll == UnreadOrAll.Unread}
|
||||
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
|
||||
/>
|
||||
{i18n.t('unread')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer
|
||||
${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={UnreadOrAll.All}
|
||||
checked={this.state.unreadOrAll == UnreadOrAll.All}
|
||||
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
|
||||
/>
|
||||
{i18n.t('all')}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
messageTypeRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.messageType == MessageType.All && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={MessageType.All}
|
||||
checked={this.state.messageType == MessageType.All}
|
||||
onChange={linkEvent(this, this.handleMessageTypeChange)}
|
||||
/>
|
||||
{i18n.t('all')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.messageType == MessageType.Replies && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={MessageType.Replies}
|
||||
checked={this.state.messageType == MessageType.Replies}
|
||||
onChange={linkEvent(this, this.handleMessageTypeChange)}
|
||||
/>
|
||||
{i18n.t('replies')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.messageType == MessageType.Mentions && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={MessageType.Mentions}
|
||||
checked={this.state.messageType == MessageType.Mentions}
|
||||
onChange={linkEvent(this, this.handleMessageTypeChange)}
|
||||
/>
|
||||
{i18n.t('mentions')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.messageType == MessageType.Messages && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={MessageType.Messages}
|
||||
checked={this.state.messageType == MessageType.Messages}
|
||||
onChange={linkEvent(this, this.handleMessageTypeChange)}
|
||||
/>
|
||||
{i18n.t('messages')}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
selects() {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<select
|
||||
value={this.state.unreadOrAll}
|
||||
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
|
||||
class="custom-select custom-select-sm w-auto mr-2"
|
||||
>
|
||||
<option disabled>{i18n.t('type')}</option>
|
||||
<option value={UnreadOrAll.Unread}>{i18n.t('unread')}</option>
|
||||
<option value={UnreadOrAll.All}>{i18n.t('all')}</option>
|
||||
</select>
|
||||
<select
|
||||
value={this.state.unreadType}
|
||||
onChange={linkEvent(this, this.handleUnreadTypeChange)}
|
||||
class="custom-select custom-select-sm w-auto mr-2"
|
||||
>
|
||||
<option disabled>{i18n.t('type')}</option>
|
||||
<option value={UnreadType.All}>{i18n.t('all')}</option>
|
||||
<option value={UnreadType.Replies}>{i18n.t('replies')}</option>
|
||||
<option value={UnreadType.Mentions}>{i18n.t('mentions')}</option>
|
||||
<option value={UnreadType.Messages}>{i18n.t('messages')}</option>
|
||||
</select>
|
||||
<span class="mr-3">{this.unreadOrAllRadios()}</span>
|
||||
<span class="mr-3">{this.messageTypeRadios()}</span>
|
||||
<SortSelect
|
||||
sort={this.state.sort}
|
||||
onChange={this.handleSortChange}
|
||||
|
@ -196,7 +261,12 @@ export class Inbox extends Component<any, InboxState> {
|
|||
<div>
|
||||
{combined.map(i =>
|
||||
isCommentType(i) ? (
|
||||
<CommentNodes nodes={[{ comment: i }]} noIndent markable />
|
||||
<CommentNodes
|
||||
nodes={[{ comment: i }]}
|
||||
noIndent
|
||||
markable
|
||||
showContext
|
||||
/>
|
||||
) : (
|
||||
<PrivateMessage privateMessage={i} />
|
||||
)
|
||||
|
@ -212,6 +282,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
nodes={commentsToFlatNodes(this.state.replies)}
|
||||
noIndent
|
||||
markable
|
||||
showContext
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -221,7 +292,12 @@ export class Inbox extends Component<any, InboxState> {
|
|||
return (
|
||||
<div>
|
||||
{this.state.mentions.map(mention => (
|
||||
<CommentNodes nodes={[{ comment: mention }]} noIndent markable />
|
||||
<CommentNodes
|
||||
nodes={[{ comment: mention }]}
|
||||
noIndent
|
||||
markable
|
||||
showContext
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -277,8 +353,8 @@ export class Inbox extends Component<any, InboxState> {
|
|||
i.refetch();
|
||||
}
|
||||
|
||||
handleUnreadTypeChange(i: Inbox, event: any) {
|
||||
i.state.unreadType = Number(event.target.value);
|
||||
handleMessageTypeChange(i: Inbox, event: any) {
|
||||
i.state.messageType = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.refetch();
|
||||
|
|
48
ui/src/components/navbar.tsx
vendored
48
ui/src/components/navbar.tsx
vendored
|
@ -26,6 +26,8 @@ import {
|
|||
fetchLimit,
|
||||
isCommentType,
|
||||
toast,
|
||||
messageToastify,
|
||||
md,
|
||||
} from '../utils';
|
||||
import { version } from '../version';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -100,6 +102,22 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
<Link title={version} class="navbar-brand" to="/">
|
||||
{this.state.siteName}
|
||||
</Link>
|
||||
{this.state.isLoggedIn && (
|
||||
<Link
|
||||
class="ml-auto p-0 navbar-toggler nav-link"
|
||||
to="/inbox"
|
||||
title={i18n.t('inbox')}
|
||||
>
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-bell"></use>
|
||||
</svg>
|
||||
{this.state.unreadCount > 0 && (
|
||||
<span class="ml-1 badge badge-light">
|
||||
{this.state.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
|
@ -350,21 +368,33 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
|
||||
notify(reply: Comment | PrivateMessage) {
|
||||
let creator_name = reply.creator_name;
|
||||
let creator_avatar = reply.creator_avatar
|
||||
? reply.creator_avatar
|
||||
: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
|
||||
let link = isCommentType(reply)
|
||||
? `/post/${reply.post_id}/comment/${reply.id}`
|
||||
: `/inbox`;
|
||||
let body = md.render(reply.content);
|
||||
|
||||
messageToastify(
|
||||
creator_name,
|
||||
creator_avatar,
|
||||
body,
|
||||
link,
|
||||
this.context.router
|
||||
);
|
||||
|
||||
if (Notification.permission !== 'granted') Notification.requestPermission();
|
||||
else {
|
||||
var notification = new Notification(reply.creator_name, {
|
||||
icon: reply.creator_avatar
|
||||
? reply.creator_avatar
|
||||
: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
|
||||
body: `${reply.content}`,
|
||||
icon: creator_avatar,
|
||||
body: body,
|
||||
});
|
||||
|
||||
notification.onclick = () => {
|
||||
this.context.router.history.push(
|
||||
isCommentType(reply)
|
||||
? `/post/${reply.post_id}/comment/${reply.id}`
|
||||
: `/inbox`
|
||||
);
|
||||
event.preventDefault();
|
||||
this.context.router.history.push(link);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
16
ui/src/components/post-listing.tsx
vendored
16
ui/src/components/post-listing.tsx
vendored
|
@ -524,7 +524,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
class="btn btn-link btn-animate btn-sm text-muted"
|
||||
class="btn btn-sm btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleSavePostClick)}
|
||||
data-tippy-content={
|
||||
post.saved ? i18n.t('unsave') : i18n.t('save')
|
||||
|
@ -540,7 +540,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
class="btn btn-link btn-animate btn-sm text-muted"
|
||||
class="btn btn-sm btn-link btn-animate text-muted"
|
||||
to={`/create_post${this.crossPostParams}`}
|
||||
title={i18n.t('cross_post')}
|
||||
>
|
||||
|
@ -555,7 +555,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
class="btn btn-link btn-animate btn-sm text-muted"
|
||||
class="btn btn-sm btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={i18n.t('edit')}
|
||||
>
|
||||
|
@ -566,7 +566,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</li>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
class="btn btn-link btn-animate btn-sm text-muted"
|
||||
class="btn btn-sm btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
data-tippy-content={
|
||||
!post.deleted
|
||||
|
@ -588,7 +588,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{!this.state.showAdvanced && this.props.showBody ? (
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
class="btn btn-link btn-animate btn-sm text-muted"
|
||||
class="btn btn-sm btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleShowAdvanced)}
|
||||
data-tippy-content={i18n.t('more')}
|
||||
>
|
||||
|
@ -602,7 +602,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{this.props.showBody && post.body && (
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
class="btn btn-link btn-animate btn-sm text-muted"
|
||||
class="btn btn-sm btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
data-tippy-content={i18n.t('view_source')}
|
||||
>
|
||||
|
@ -619,7 +619,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
class="btn btn-link btn-animate btn-sm text-muted"
|
||||
class="btn btn-sm btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleModLock)}
|
||||
data-tippy-content={
|
||||
post.locked
|
||||
|
@ -637,7 +637,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</li>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
class="btn btn-link btn-animate btn-sm text-muted"
|
||||
class="btn btn-sm btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleModSticky)}
|
||||
data-tippy-content={
|
||||
post.stickied
|
||||
|
|
12
ui/src/components/symbols.tsx
vendored
12
ui/src/components/symbols.tsx
vendored
|
@ -88,14 +88,14 @@ export class Symbols extends Component<any, any> {
|
|||
<path d="M17 19h-12c-0.553 0-1-0.447-1-1s0.447-1 1-1h12c0.553 0 1 0.447 1 1s-0.447 1-1 1z"></path>
|
||||
<path d="M17.5 5h-12.5v9c0 1.1 0.9 2 2 2h8c1.1 0 2-0.9 2-2v-2h0.5c1.93 0 3.5-1.57 3.5-3.5s-1.57-3.5-3.5-3.5zM15 14h-8v-7h8v7zM17.5 10h-1.5v-3h1.5c0.827 0 1.5 0.673 1.5 1.5s-0.673 1.5-1.5 1.5z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-rss" viewBox="0 0 32 32">
|
||||
<path d="M4.259 23.467c-2.35 0-4.259 1.917-4.259 4.252 0 2.349 1.909 4.244 4.259 4.244 2.358 0 4.265-1.895 4.265-4.244-0-2.336-1.907-4.252-4.265-4.252zM0.005 10.873v6.133c3.993 0 7.749 1.562 10.577 4.391 2.825 2.822 4.384 6.595 4.384 10.603h6.16c-0-11.651-9.478-21.127-21.121-21.127zM0.012 0v6.136c14.243 0 25.836 11.604 25.836 25.864h6.152c0-17.64-14.352-32-31.988-32z"></path>
|
||||
<symbol id="icon-rss" viewBox="0 0 24 24">
|
||||
<path d="M4 12c2.209 0 4.208 0.894 5.657 2.343s2.343 3.448 2.343 5.657c0 0.552 0.448 1 1 1s1-0.448 1-1c0-2.761-1.12-5.263-2.929-7.071s-4.31-2.929-7.071-2.929c-0.552 0-1 0.448-1 1s0.448 1 1 1zM4 5c4.142 0 7.891 1.678 10.607 4.393s4.393 6.465 4.393 10.607c0 0.552 0.448 1 1 1s1-0.448 1-1c0-4.694-1.904-8.946-4.979-12.021s-7.327-4.979-12.021-4.979c-0.552 0-1 0.448-1 1s0.448 1 1 1zM7 19c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-arrow-down" viewBox="0 0 26 28">
|
||||
<path d="M25.172 13c0 0.531-0.219 1.047-0.578 1.406l-10.172 10.187c-0.375 0.359-0.891 0.578-1.422 0.578s-1.047-0.219-1.406-0.578l-10.172-10.187c-0.375-0.359-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l1.156-1.172c0.375-0.359 0.891-0.578 1.422-0.578s1.047 0.219 1.406 0.578l4.594 4.594v-11c0-1.094 0.906-2 2-2h2c1.094 0 2 0.906 2 2v11l4.594-4.594c0.359-0.359 0.875-0.578 1.406-0.578s1.047 0.219 1.422 0.578l1.172 1.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
|
||||
<symbol id="icon-arrow-down" viewBox="0 0 24 24">
|
||||
<path d="M18.293 11.293l-5.293 5.293v-11.586c0-0.552-0.448-1-1-1s-1 0.448-1 1v11.586l-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l7 7c0.092 0.092 0.202 0.166 0.324 0.217 0.245 0.101 0.521 0.101 0.766 0 0.118-0.049 0.228-0.121 0.324-0.217l7-7c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-arrow-up" viewBox="0 0 26 28">
|
||||
<path d="M25.172 15.172c0 0.531-0.219 1.031-0.578 1.406l-1.172 1.172c-0.375 0.375-0.891 0.594-1.422 0.594s-1.047-0.219-1.406-0.594l-4.594-4.578v11c0 1.125-0.938 1.828-2 1.828h-2c-1.062 0-2-0.703-2-1.828v-11l-4.594 4.578c-0.359 0.375-0.875 0.594-1.406 0.594s-1.047-0.219-1.406-0.594l-1.172-1.172c-0.375-0.375-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l10.172-10.172c0.359-0.375 0.875-0.578 1.406-0.578s1.047 0.203 1.422 0.578l10.172 10.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
|
||||
<symbol id="icon-arrow-up" viewBox="0 0 24 24">
|
||||
<path d="M5.707 12.707l5.293-5.293v11.586c0 0.552 0.448 1 1 1s1-0.448 1-1v-11.586l5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-7-7c-0.092-0.092-0.202-0.166-0.324-0.217s-0.253-0.076-0.383-0.076c-0.256 0-0.512 0.098-0.707 0.293l-7 7c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-mail" viewBox="0 0 24 24">
|
||||
<path d="M3 7.921l8.427 5.899c0.34 0.235 0.795 0.246 1.147 0l8.426-5.899v10.079c0 0.272-0.11 0.521-0.295 0.705s-0.433 0.295-0.705 0.295h-16c-0.272 0-0.521-0.11-0.705-0.295s-0.295-0.433-0.295-0.705zM1 5.983c0 0.010 0 0.020 0 0.030v11.987c0 0.828 0.34 1.579 0.88 2.12s1.292 0.88 2.12 0.88h16c0.828 0 1.579-0.34 2.12-0.88s0.88-1.292 0.88-2.12v-11.988c0-0.010 0-0.020 0-0.030-0.005-0.821-0.343-1.565-0.88-2.102-0.541-0.54-1.292-0.88-2.12-0.88h-16c-0.828 0-1.579 0.34-2.12 0.88-0.537 0.537-0.875 1.281-0.88 2.103zM20.894 5.554l-8.894 6.225-8.894-6.225c0.048-0.096 0.112-0.183 0.188-0.259 0.185-0.185 0.434-0.295 0.706-0.295h16c0.272 0 0.521 0.11 0.705 0.295 0.076 0.076 0.14 0.164 0.188 0.259z"></path>
|
||||
|
|
85
ui/src/components/user.tsx
vendored
85
ui/src/components/user.tsx
vendored
|
@ -242,27 +242,74 @@ export class User extends Component<any, UserState> {
|
|||
);
|
||||
}
|
||||
|
||||
viewRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.view == View.Overview && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={View.Overview}
|
||||
checked={this.state.view == View.Overview}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
/>
|
||||
{i18n.t('overview')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.view == View.Comments && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={View.Comments}
|
||||
checked={this.state.view == View.Comments}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
/>
|
||||
{i18n.t('comments')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.view == View.Posts && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={View.Posts}
|
||||
checked={this.state.view == View.Posts}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
/>
|
||||
{i18n.t('posts')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.view == View.Saved && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={View.Saved}
|
||||
checked={this.state.view == View.Saved}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
/>
|
||||
{i18n.t('saved')}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
selects() {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<select
|
||||
value={this.state.view}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
class="custom-select custom-select-sm w-auto"
|
||||
>
|
||||
<option disabled>{i18n.t('view')}</option>
|
||||
<option value={View.Overview}>{i18n.t('overview')}</option>
|
||||
<option value={View.Comments}>{i18n.t('comments')}</option>
|
||||
<option value={View.Posts}>{i18n.t('posts')}</option>
|
||||
<option value={View.Saved}>{i18n.t('saved')}</option>
|
||||
</select>
|
||||
<span class="ml-2">
|
||||
<SortSelect
|
||||
sort={this.state.sort}
|
||||
onChange={this.handleSortChange}
|
||||
hideHot
|
||||
/>
|
||||
</span>
|
||||
<span class="mr-3">{this.viewRadios()}</span>
|
||||
<SortSelect
|
||||
sort={this.state.sort}
|
||||
onChange={this.handleSortChange}
|
||||
hideHot
|
||||
/>
|
||||
<a
|
||||
href={`/feeds/u/${this.state.username}.xml?sort=${
|
||||
SortType[this.state.sort]
|
||||
|
@ -312,6 +359,7 @@ export class User extends Component<any, UserState> {
|
|||
nodes={[{ comment: i.data as Comment }]}
|
||||
admins={this.state.admins}
|
||||
noIndent
|
||||
showContext
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -327,6 +375,7 @@ export class User extends Component<any, UserState> {
|
|||
nodes={commentsToFlatNodes(this.state.comments)}
|
||||
admins={this.state.admins}
|
||||
noIndent
|
||||
showContext
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
24
ui/src/utils.ts
vendored
24
ui/src/utils.ts
vendored
|
@ -218,7 +218,7 @@ export function validURL(str: string) {
|
|||
}
|
||||
|
||||
export function validEmail(email: string) {
|
||||
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
let re = /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
}
|
||||
|
||||
|
@ -436,6 +436,28 @@ export function toast(text: string, background: string = 'success') {
|
|||
}).showToast();
|
||||
}
|
||||
|
||||
export function messageToastify(
|
||||
creator: string,
|
||||
avatar: string,
|
||||
body: string,
|
||||
link: string,
|
||||
router: any
|
||||
) {
|
||||
let backgroundColor = `var(--light)`;
|
||||
Toastify({
|
||||
text: `${body}<br />${creator}`,
|
||||
avatar: avatar,
|
||||
backgroundColor: backgroundColor,
|
||||
close: true,
|
||||
gravity: 'top',
|
||||
position: 'right',
|
||||
duration: 0,
|
||||
onClick: () => {
|
||||
router.history.push(link);
|
||||
},
|
||||
}).showToast();
|
||||
}
|
||||
|
||||
export function setupTribute(): Tribute {
|
||||
return new Tribute({
|
||||
collection: [
|
||||
|
|
1
ui/translations/en.json
vendored
1
ui/translations/en.json
vendored
|
@ -39,6 +39,7 @@
|
|||
"avatar": "Avatar",
|
||||
"upload_avatar": "Upload Avatar",
|
||||
"show_avatars": "Show Avatars",
|
||||
"show_context": "Show context",
|
||||
"formatting_help": "formatting help",
|
||||
"sorting_help": "sorting help",
|
||||
"view_source": "view source",
|
||||
|
|
Reference in a new issue