Merge branch 'autocomplete' into dev

This commit is contained in:
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('clean', _ => Sparky.src('dist/').clean('dist/'));
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'], _ => {
fuse.dev();
app.hmr().watch();

1
ui/package.json vendored
View File

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

View File

@ -1,10 +1,12 @@
import { Component, linkEvent } from 'inferno';
import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces';
import { capitalizeFirstLetter } from '../utils';
import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
import { Subscription } from "rxjs";
import { capitalizeFirstLetter, fetchLimit, msgOp } from '../utils';
import { WebSocketService, UserService } from '../services';
import * as autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
import * as tributejs from 'tributejs';
interface CommentFormProps {
postId?: number;
@ -21,6 +23,10 @@ interface 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 = {
commentForm: {
auth: null,
@ -34,6 +40,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
constructor(props: any, context: any) {
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;
@ -51,7 +85,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
}
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() {
@ -60,7 +101,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
<div class="form-group row">
<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 class="row">
@ -100,4 +141,66 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
handleReplyCancel(i: CommentForm) {
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 Normal file
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="apple-touch-icon" href="/static/assets/apple-touch-icon.png" />
<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>
<body>

1
ui/src/index.tsx vendored
View File

@ -19,6 +19,7 @@ import { Sponsors } from './components/sponsors';
import { Symbols } from './components/symbols';
import { i18n } from './i18next';
import './css/tribute.css';
import './css/bootstrap.min.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 * as markdown_it from 'markdown-it';
declare var markdownitEmoji: any;
import * as markdown_it_container from 'markdown-it-container';
export let repoUrl = 'https://github.com/dessalines/lemmy';
@ -39,7 +40,7 @@ var md = new markdown_it({
return '</details>\n';
}
}
});
}).use(markdownitEmoji);
export function hotRank(comment: Comment): number {
// 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