diff options
-rw-r--r-- | ui/src/components/comment-form.tsx | 130 | ||||
-rw-r--r-- | ui/src/components/community-form.tsx | 22 | ||||
-rw-r--r-- | ui/src/components/post-form.tsx | 16 | ||||
-rw-r--r-- | ui/src/components/private-message-form.tsx | 16 | ||||
-rw-r--r-- | ui/src/components/site-form.tsx | 18 | ||||
-rw-r--r-- | ui/src/utils.ts | 127 |
6 files changed, 193 insertions, 136 deletions
diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index b8ea0a5a..f4eb1181 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -2,28 +2,20 @@ import { Component, linkEvent } from 'inferno'; import { CommentNode as CommentNodeI, CommentForm as CommentFormI, - SearchForm, - SearchType, - SortType, - UserOperation, - SearchResponse, } from '../interfaces'; -import { Subscription } from 'rxjs'; import { - wsJsonToRes, capitalizeFirstLetter, - mentionDropdownFetchLimit, mdToHtml, randomStr, markdownHelpUrl, toast, + setupTribute, } from '../utils'; import { WebSocketService, UserService } from '../services'; import autosize from 'autosize'; +import Tribute from 'tributejs/src/Tribute.js'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; -import Tribute from 'tributejs/src/Tribute.js'; -import emojiShortName from 'emoji-short-name'; interface CommentFormProps { postId?: number; @@ -42,9 +34,7 @@ interface CommentFormState { export class CommentForm extends Component<CommentFormProps, CommentFormState> { private id = `comment-form-${randomStr()}`; - private userSub: Subscription; - private communitySub: Subscription; - private tribute: any; + private tribute: Tribute; private emptyState: CommentFormState = { commentForm: { auth: null, @@ -68,55 +58,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> { constructor(props: any, context: any) { super(props, context); - this.tribute = new Tribute({ - collection: [ - // Emojis - { - trigger: ':', - menuItemTemplate: (item: any) => { - let emoji = `:${item.original.key}:`; - return `${item.original.val} ${emoji}`; - }, - selectTemplate: (item: any) => { - return `:${item.original.key}:`; - }, - values: Object.entries(emojiShortName).map(e => { - return { key: e[1], val: e[0] }; - }), - allowSpaces: false, - autocompleteMode: true, - menuItemLimit: mentionDropdownFetchLimit, - }, - // Users - { - trigger: '@', - selectTemplate: (item: any) => { - return `[/u/${item.original.key}](/u/${item.original.key})`; - }, - values: (text: string, cb: any) => { - this.userSearch(text, (users: any) => cb(users)); - }, - allowSpaces: false, - autocompleteMode: true, - menuItemLimit: mentionDropdownFetchLimit, - }, - - // Communities - { - trigger: '#', - selectTemplate: (item: any) => { - return `[/c/${item.original.key}](/c/${item.original.key})`; - }, - values: (text: string, cb: any) => { - this.communitySearch(text, (communities: any) => cb(communities)); - }, - allowSpaces: false, - autocompleteMode: true, - menuItemLimit: mentionDropdownFetchLimit, - }, - ], - }); - + this.tribute = setupTribute(); this.state = this.emptyState; if (this.props.node) { @@ -297,68 +239,4 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> { toast(error, 'danger'); }); } - - userSearch(text: string, cb: any) { - if (text) { - let form: SearchForm = { - q: text, - type_: SearchType[SearchType.Users], - sort: SortType[SortType.TopAll], - page: 1, - limit: mentionDropdownFetchLimit, - }; - - WebSocketService.Instance.search(form); - - this.userSub = WebSocketService.Instance.subject.subscribe( - msg => { - let res = wsJsonToRes(msg); - if (res.op == UserOperation.Search) { - let data = res.data as SearchResponse; - let users = data.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: mentionDropdownFetchLimit, - }; - - WebSocketService.Instance.search(form); - - this.communitySub = WebSocketService.Instance.subject.subscribe( - msg => { - let res = wsJsonToRes(msg); - if (res.op == UserOperation.Search) { - let data = res.data as SearchResponse; - let communities = data.communities.map(u => { - return { key: u.name }; - }); - cb(communities); - this.communitySub.unsubscribe(); - } - }, - err => console.error(err), - () => console.log('complete') - ); - } else { - cb([]); - } - } } diff --git a/ui/src/components/community-form.tsx b/ui/src/components/community-form.tsx index 4dc7bfcb..33c63c89 100644 --- a/ui/src/components/community-form.tsx +++ b/ui/src/components/community-form.tsx @@ -11,7 +11,14 @@ import { WebSocketJsonResponse, } from '../interfaces'; import { WebSocketService } from '../services'; -import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils'; +import { + wsJsonToRes, + capitalizeFirstLetter, + toast, + randomStr, + setupTribute, +} from '../utils'; +import Tribute from 'tributejs/src/Tribute.js'; import autosize from 'autosize'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -36,6 +43,8 @@ export class CommunityForm extends Component< CommunityFormProps, CommunityFormState > { + private id = `community-form-${randomStr()}`; + private tribute: Tribute; private subscription: Subscription; private emptyState: CommunityFormState = { @@ -53,6 +62,7 @@ export class CommunityForm extends Component< constructor(props: any, context: any) { super(props, context); + this.tribute = setupTribute(); this.state = this.emptyState; if (this.props.community) { @@ -80,7 +90,14 @@ export class CommunityForm extends Component< } componentDidMount() { - autosize(document.querySelectorAll('textarea')); + var textarea: any = document.getElementById(this.id); + autosize(textarea); + this.tribute.attach(textarea); + textarea.addEventListener('tribute-replaced', () => { + this.state.communityForm.description = textarea.value; + this.setState(this.state); + autosize.update(textarea); + }); } componentWillUnmount() { @@ -130,6 +147,7 @@ export class CommunityForm extends Component< </label> <div class="col-12"> <textarea + id={this.id} value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 44061774..677007ca 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -30,8 +30,11 @@ import { debounce, isImage, toast, + randomStr, + setupTribute, } from '../utils'; import autosize from 'autosize'; +import Tribute from 'tributejs/src/Tribute.js'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -56,6 +59,8 @@ interface PostFormState { } export class PostForm extends Component<PostFormProps, PostFormState> { + private id = `post-form-${randomStr()}`; + private tribute: Tribute; private subscription: Subscription; private emptyState: PostFormState = { postForm: { @@ -82,6 +87,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> { this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this); this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this); + this.tribute = setupTribute(); this.state = this.emptyState; if (this.props.post) { @@ -126,7 +132,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> { } componentDidMount() { - autosize(document.querySelectorAll('textarea')); + var textarea: any = document.getElementById(this.id); + autosize(textarea); + this.tribute.attach(textarea); + textarea.addEventListener('tribute-replaced', () => { + this.state.postForm.body = textarea.value; + this.setState(this.state); + autosize.update(textarea); + }); } componentWillUnmount() { @@ -238,6 +251,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> { </label> <div class="col-sm-10"> <textarea + id={this.id} value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} className={`form-control ${this.state.previewMode && 'd-none'}`} diff --git a/ui/src/components/private-message-form.tsx b/ui/src/components/private-message-form.tsx index 170c2ab8..c8627845 100644 --- a/ui/src/components/private-message-form.tsx +++ b/ui/src/components/private-message-form.tsx @@ -24,7 +24,10 @@ import { pictshareAvatarThumbnail, wsJsonToRes, toast, + randomStr, + setupTribute, } from '../utils'; +import Tribute from 'tributejs/src/Tribute.js'; import autosize from 'autosize'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -49,6 +52,8 @@ export class PrivateMessageForm extends Component< PrivateMessageFormProps, PrivateMessageFormState > { + private id = `message-form-${randomStr()}`; + private tribute: Tribute; private subscription: Subscription; private emptyState: PrivateMessageFormState = { privateMessageForm: { @@ -64,6 +69,7 @@ export class PrivateMessageForm extends Component< constructor(props: any, context: any) { super(props, context); + this.tribute = setupTribute(); this.state = this.emptyState; if (this.props.privateMessage) { @@ -93,7 +99,14 @@ export class PrivateMessageForm extends Component< } componentDidMount() { - autosize(document.querySelectorAll('textarea')); + var textarea: any = document.getElementById(this.id); + autosize(textarea); + this.tribute.attach(textarea); + textarea.addEventListener('tribute-replaced', () => { + this.state.privateMessageForm.content = textarea.value; + this.setState(this.state); + autosize.update(textarea); + }); } componentWillUnmount() { @@ -136,6 +149,7 @@ export class PrivateMessageForm extends Component< <label class="col-sm-2 col-form-label">{i18n.t('message')}</label> <div class="col-sm-10"> <textarea + id={this.id} value={this.state.privateMessageForm.content} onInput={linkEvent(this, this.handleContentChange)} className={`form-control ${this.state.previewMode && 'd-none'}`} diff --git a/ui/src/components/site-form.tsx b/ui/src/components/site-form.tsx index 5d8ff0b5..936a9d53 100644 --- a/ui/src/components/site-form.tsx +++ b/ui/src/components/site-form.tsx @@ -1,8 +1,9 @@ import { Component, linkEvent } from 'inferno'; import { Site, SiteForm as SiteFormI } from '../interfaces'; import { WebSocketService } from '../services'; -import { capitalizeFirstLetter } from '../utils'; +import { capitalizeFirstLetter, randomStr, setupTribute } from '../utils'; import autosize from 'autosize'; +import Tribute from 'tributejs/src/Tribute.js'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -17,6 +18,8 @@ interface SiteFormState { } export class SiteForm extends Component<SiteFormProps, SiteFormState> { + private id = `site-form-${randomStr()}`; + private tribute: Tribute; private emptyState: SiteFormState = { siteForm: { enable_downvotes: true, @@ -29,7 +32,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> { constructor(props: any, context: any) { super(props, context); + + this.tribute = setupTribute(); this.state = this.emptyState; + if (this.props.site) { this.state.siteForm = { name: this.props.site.name, @@ -42,7 +48,14 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> { } componentDidMount() { - autosize(document.querySelectorAll('textarea')); + var textarea: any = document.getElementById(this.id); + autosize(textarea); + this.tribute.attach(textarea); + textarea.addEventListener('tribute-replaced', () => { + this.state.siteForm.description = textarea.value; + this.setState(this.state); + autosize.update(textarea); + }); } render() { @@ -75,6 +88,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> { </label> <div class="col-12"> <textarea + id={this.id} value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 0e4cd9d5..6a0ffdb3 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -18,13 +18,17 @@ import { SearchType, WebSocketResponse, WebSocketJsonResponse, + SearchForm, + SearchResponse, } from './interfaces'; -import { UserService } from './services/UserService'; +import { UserService, WebSocketService } from './services'; + +import Tribute from 'tributejs/src/Tribute.js'; import markdown_it from 'markdown-it'; import markdownitEmoji from 'markdown-it-emoji/light'; import markdown_it_container from 'markdown-it-container'; -import * as twemoji from 'twemoji'; -import * as emojiShortName from 'emoji-short-name'; +import twemoji from 'twemoji'; +import emojiShortName from 'emoji-short-name'; import Toastify from 'toastify-js'; export const repoUrl = 'https://github.com/dessalines/lemmy'; @@ -33,7 +37,7 @@ export const archiveUrl = 'https://archive.is'; export const postRefetchSeconds: number = 60 * 1000; export const fetchLimit: number = 20; -export const mentionDropdownFetchLimit = 6; +export const mentionDropdownFetchLimit = 10; export function randomStr() { return Math.random() @@ -380,3 +384,118 @@ export function toast(text: string, background: string = 'success') { backgroundColor: backgroundColor, }).showToast(); } + +export function setupTribute(): Tribute { + return new Tribute({ + collection: [ + // Emojis + { + trigger: ':', + menuItemTemplate: (item: any) => { + let emoji = `:${item.original.key}:`; + return `${item.original.val} ${emoji}`; + }, + selectTemplate: (item: any) => { + return `:${item.original.key}:`; + }, + values: Object.entries(emojiShortName).map(e => { + return { key: e[1], val: e[0] }; + }), + allowSpaces: false, + autocompleteMode: true, + menuItemLimit: mentionDropdownFetchLimit, + }, + // Users + { + trigger: '@', + selectTemplate: (item: any) => { + return `[/u/${item.original.key}](/u/${item.original.key})`; + }, + values: (text: string, cb: any) => { + userSearch(text, (users: any) => cb(users)); + }, + allowSpaces: false, + autocompleteMode: true, + menuItemLimit: mentionDropdownFetchLimit, + }, + + // Communities + { + trigger: '#', + selectTemplate: (item: any) => { + return `[/c/${item.original.key}](/c/${item.original.key})`; + }, + values: (text: string, cb: any) => { + communitySearch(text, (communities: any) => cb(communities)); + }, + allowSpaces: false, + autocompleteMode: true, + menuItemLimit: mentionDropdownFetchLimit, + }, + ], + }); +} + +function userSearch(text: string, cb: any) { + if (text) { + let form: SearchForm = { + q: text, + type_: SearchType[SearchType.Users], + sort: SortType[SortType.TopAll], + page: 1, + limit: mentionDropdownFetchLimit, + }; + + WebSocketService.Instance.search(form); + + this.userSub = WebSocketService.Instance.subject.subscribe( + msg => { + let res = wsJsonToRes(msg); + if (res.op == UserOperation.Search) { + let data = res.data as SearchResponse; + let users = data.users.map(u => { + return { key: u.name }; + }); + cb(users); + this.userSub.unsubscribe(); + } + }, + err => console.error(err), + () => console.log('complete') + ); + } else { + cb([]); + } +} + +function communitySearch(text: string, cb: any) { + if (text) { + let form: SearchForm = { + q: text, + type_: SearchType[SearchType.Communities], + sort: SortType[SortType.TopAll], + page: 1, + limit: mentionDropdownFetchLimit, + }; + + WebSocketService.Instance.search(form); + + this.communitySub = WebSocketService.Instance.subject.subscribe( + msg => { + let res = wsJsonToRes(msg); + if (res.op == UserOperation.Search) { + let data = res.data as SearchResponse; + let communities = data.communities.map(u => { + return { key: u.name }; + }); + cb(communities); + this.communitySub.unsubscribe(); + } + }, + err => console.error(err), + () => console.log('complete') + ); + } else { + cb([]); + } +} |