Merge branch 'autocomplete' into dev

pull/722/head
Dessalines 2019-08-29 20:13:37 -07:00
commit 4e982ccf51
10 changed files with 785 additions and 112 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
ui/fuse.js vendored
View File

@ -48,7 +48,7 @@ Sparky.task('config', _ => {
// Sparky.task('version', _ => setVersion()); // Sparky.task('version', _ => setVersion());
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/')); Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
Sparky.task('env', _ => (isProduction = true)); Sparky.task('env', _ => (isProduction = true));
Sparky.task('copy-assets', () => Sparky.src('assets/**/**.*').dest('dist/')); Sparky.task('copy-assets', () => Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static'));
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => { Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
fuse.dev(); fuse.dev();
app.hmr().watch(); app.hmr().watch();

1
ui/package.json vendored
View File

@ -34,6 +34,7 @@
"moment": "^2.24.0", "moment": "^2.24.0",
"rxjs": "^6.4.0", "rxjs": "^6.4.0",
"terser": "^3.17.0", "terser": "^3.17.0",
"tributejs": "^3.7.2",
"ws": "^7.0.0" "ws": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,10 +1,12 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces'; import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
import { capitalizeFirstLetter } from '../utils'; import { Subscription } from "rxjs";
import { capitalizeFirstLetter, fetchLimit, msgOp } from '../utils';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
import * as tributejs from 'tributejs';
interface CommentFormProps { interface CommentFormProps {
postId?: number; postId?: number;
@ -21,6 +23,10 @@ interface CommentFormState {
export class CommentForm extends Component<CommentFormProps, CommentFormState> { export class CommentForm extends Component<CommentFormProps, CommentFormState> {
private id = `comment-form-${btoa(Math.random()).substring(0,12)}`;
private userSub: Subscription;
private communitySub: Subscription;
private tribute: any;
private emptyState: CommentFormState = { private emptyState: CommentFormState = {
commentForm: { commentForm: {
auth: null, auth: null,
@ -34,6 +40,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.tribute = new tributejs({
collection: [
// Users
{
trigger: '@',
selectTemplate: (item: any) => {
return `[/u/${item.original.key}](${window.location.origin}/u/${item.original.key})`;
},
values: (text: string, cb: any) => {
this.userSearch(text, users => cb(users));
},
autocompleteMode: true,
},
// Communities
{
trigger: '#',
selectTemplate: (item: any) => {
return `[/c/${item.original.key}](${window.location.origin}/c/${item.original.key})`;
},
values: (text: string, cb: any) => {
this.communitySearch(text, communities => cb(communities));
},
autocompleteMode: true,
}
]
});
this.state = this.emptyState; this.state = this.emptyState;
@ -51,7 +85,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
} }
componentDidMount() { componentDidMount() {
autosize(document.querySelectorAll('textarea')); var textarea: any = document.getElementById(this.id);
autosize(textarea);
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.commentForm.content = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
} }
render() { render() {
@ -60,7 +101,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}> <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-12"> <div class="col-sm-12">
<textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} maxLength={10000} /> <textarea id={this.id} class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} maxLength={10000} />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -100,4 +141,66 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
handleReplyCancel(i: CommentForm) { handleReplyCancel(i: CommentForm) {
i.props.onReplyCancel(); i.props.onReplyCancel();
} }
userSearch(text: string, cb: any) {
if (text) {
let form: SearchForm = {
q: text,
type_: SearchType[SearchType.Users],
sort: SortType[SortType.TopAll],
page: 1,
limit: fetchLimit,
};
WebSocketService.Instance.search(form);
this.userSub = WebSocketService.Instance.subject
.subscribe(
(msg) => {
let op: UserOperation = msgOp(msg);
if (op == UserOperation.Search) {
let res: SearchResponse = msg;
let users = res.users.map(u => {return {key: u.name}});
cb(users);
this.userSub.unsubscribe();
}
},
(err) => console.error(err),
() => console.log('complete')
);
} else {
cb([]);
}
}
communitySearch(text: string, cb: any) {
if (text) {
let form: SearchForm = {
q: text,
type_: SearchType[SearchType.Communities],
sort: SortType[SortType.TopAll],
page: 1,
limit: fetchLimit,
};
WebSocketService.Instance.search(form);
this.communitySub = WebSocketService.Instance.subject
.subscribe(
(msg) => {
let op: UserOperation = msgOp(msg);
if (op == UserOperation.Search) {
let res: SearchResponse = msg;
let communities = res.communities.map(u => {return {key: u.name}});
cb(communities);
this.communitySub.unsubscribe();
}
},
(err) => console.error(err),
() => console.log('complete')
);
} else {
cb([]);
}
}
} }

27
ui/src/css/tribute.css vendored 100644
View File

@ -0,0 +1,27 @@
.tribute-container {
position: absolute;
top: 0;
left: 0;
height: auto;
max-height: 300px;
max-width: 500px;
overflow: auto;
display: block;
z-index: 999999; }
.tribute-container ul {
margin: 0;
margin-top: 2px;
padding: 0;
list-style: none;
background: var(--light); }
.tribute-container li {
padding: 5px 5px;
cursor: pointer; }
.tribute-container li.highlight {
background: var(--primary); }
.tribute-container li span {
font-weight: bold; }
.tribute-container li.no-match {
cursor: default; }
.tribute-container .menu-highlighted {
font-weight: bold; }

1
ui/src/index.html vendored
View File

@ -9,6 +9,7 @@
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" /> <link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" />
<link rel="apple-touch-icon" href="/static/assets/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/static/assets/apple-touch-icon.png" />
<script async src="/static/assets/libs/sortable/sortable.min.js"></script> <script async src="/static/assets/libs/sortable/sortable.min.js"></script>
<script src="/static/assets/libs/markdown-it-emoji/markdown-it-emoji.min.js" type="text/javascript"></script>
</head> </head>
<body> <body>

1
ui/src/index.tsx vendored
View File

@ -19,6 +19,7 @@ import { Sponsors } from './components/sponsors';
import { Symbols } from './components/symbols'; import { Symbols } from './components/symbols';
import { i18n } from './i18next'; import { i18n } from './i18next';
import './css/tribute.css';
import './css/bootstrap.min.css'; import './css/bootstrap.min.css';
import './css/main.css'; import './css/main.css';

3
ui/src/utils.ts vendored
View File

@ -9,6 +9,7 @@ import 'moment/locale/nl';
import { UserOperation, Comment, User, SortType, ListingType } from './interfaces'; import { UserOperation, Comment, User, SortType, ListingType } from './interfaces';
import * as markdown_it from 'markdown-it'; import * as markdown_it from 'markdown-it';
declare var markdownitEmoji: any;
import * as markdown_it_container from 'markdown-it-container'; import * as markdown_it_container from 'markdown-it-container';
export let repoUrl = 'https://github.com/dessalines/lemmy'; export let repoUrl = 'https://github.com/dessalines/lemmy';
@ -39,7 +40,7 @@ var md = new markdown_it({
return '</details>\n'; return '</details>\n';
} }
} }
}); }).use(markdownitEmoji);
export function hotRank(comment: Comment): number { export function hotRank(comment: Comment): number {
// Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity

747
ui/yarn.lock vendored

File diff suppressed because it is too large Load Diff