summaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorDessalines <tyhou13@gmx.com>2020-04-14 16:07:20 -0400
committerDessalines <tyhou13@gmx.com>2020-04-14 16:07:20 -0400
commit1336b4ed6023e7fcf0fd40be63569966ee4b1b45 (patch)
treee62e4422e0ba0430ea6d060f01d20dee22d6e564 /ui
parentf040dac647d50c97e3f9ab8058563a7fe0f29261 (diff)
parent641e4c5d96d9d152bc75318b3ea08f789d920b2b (diff)
Merge branch 'dev' into federation
Diffstat (limited to 'ui')
-rw-r--r--ui/assets/css/main.css2
-rw-r--r--ui/package.json33
-rw-r--r--ui/src/components/admin-settings.tsx241
-rw-r--r--ui/src/components/comment-form.tsx36
-rw-r--r--ui/src/components/comment-node.tsx53
-rw-r--r--ui/src/components/main.tsx24
-rw-r--r--ui/src/components/navbar.tsx28
-rw-r--r--ui/src/components/post-form.tsx43
-rw-r--r--ui/src/components/post-listing.tsx43
-rw-r--r--ui/src/components/post.tsx22
-rw-r--r--ui/src/components/private-message-form.tsx30
-rw-r--r--ui/src/components/private-message.tsx16
-rw-r--r--ui/src/components/search.tsx23
-rw-r--r--ui/src/components/sidebar.tsx32
-rw-r--r--ui/src/components/site-form.tsx7
-rw-r--r--ui/src/components/sponsors.tsx11
-rw-r--r--ui/src/components/symbols.tsx9
-rw-r--r--ui/src/components/user-listing.tsx36
-rw-r--r--ui/src/index.tsx116
-rw-r--r--ui/src/interfaces.ts21
-rw-r--r--ui/src/services/WebSocketService.ts13
-rw-r--r--ui/src/utils.ts24
-rw-r--r--ui/src/version.ts2
-rw-r--r--ui/translations/en.json6
-rw-r--r--ui/translations/es.json72
-rw-r--r--ui/translations/fr.json3
-rw-r--r--ui/translations/ka.json223
-rw-r--r--ui/yarn.lock251
28 files changed, 1085 insertions, 335 deletions
diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css
index 1c8206e3..bf249e5b 100644
--- a/ui/assets/css/main.css
+++ b/ui/assets/css/main.css
@@ -156,7 +156,7 @@ hr {
}
.emoji {
- height: 1.2em !important;
+ max-height: 1.2em !important;
}
.text-wrap-truncate {
diff --git a/ui/package.json b/ui/package.json
index 7d946614..d2eb1de9 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -14,19 +14,20 @@
},
"keywords": [],
"dependencies": {
+ "@joeattardi/emoji-button": "^2.12.1",
"@types/autosize": "^3.0.6",
- "@types/js-cookie": "^2.2.5",
+ "@types/js-cookie": "^2.2.6",
"@types/jwt-decode": "^2.2.1",
"@types/markdown-it": "^0.0.9",
"@types/markdown-it-container": "^2.0.2",
- "@types/node": "^13.9.2",
+ "@types/node": "^13.11.1",
"autosize": "^4.0.2",
"bootswatch": "^4.3.1",
- "classcat": "^1.1.3",
+ "classcat": "^4.0.2",
"dotenv": "^8.2.0",
"emoji-short-name": "^1.0.0",
- "husky": "^4.2.3",
- "i18next": "^19.3.3",
+ "husky": "^4.2.5",
+ "i18next": "^19.4.1",
"inferno": "^7.4.2",
"inferno-i18next": "nimbusec-oss/inferno-i18next",
"inferno-router": "^7.4.2",
@@ -37,26 +38,26 @@
"markdown-it-emoji": "^1.4.0",
"mobius1-selectr": "^2.4.13",
"moment": "^2.24.0",
- "prettier": "^1.18.2",
+ "prettier": "^2.0.4",
"reconnecting-websocket": "^4.4.0",
- "rxjs": "^6.4.0",
- "terser": "^4.6.7",
- "tippy.js": "^6.1.0",
+ "rxjs": "^6.5.5",
+ "terser": "^4.6.11",
+ "tippy.js": "^6.1.1",
"toastify-js": "^1.7.0",
- "tributejs": "^5.1.2",
+ "tributejs": "^5.1.3",
"twemoji": "^12.1.2",
"ws": "^7.2.3"
},
"devDependencies": {
"eslint": "^6.5.1",
"eslint-plugin-inferno": "^7.14.3",
- "eslint-plugin-jane": "^7.2.0",
+ "eslint-plugin-jane": "^7.2.1",
"fuse-box": "^3.1.3",
- "lint-staged": "^10.0.8",
- "sortpack": "^2.1.2",
- "ts-node": "^8.7.0",
- "ts-transform-classcat": "^0.0.2",
- "ts-transform-inferno": "^4.0.2",
+ "lint-staged": "^10.1.3",
+ "sortpack": "^2.1.4",
+ "ts-node": "^8.8.2",
+ "ts-transform-classcat": "^1.0.0",
+ "ts-transform-inferno": "^4.0.3",
"typescript": "^3.8.3"
},
"engines": {
diff --git a/ui/src/components/admin-settings.tsx b/ui/src/components/admin-settings.tsx
new file mode 100644
index 00000000..56af7114
--- /dev/null
+++ b/ui/src/components/admin-settings.tsx
@@ -0,0 +1,241 @@
+import { Component, linkEvent } from 'inferno';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ SiteResponse,
+ GetSiteResponse,
+ SiteConfigForm,
+ GetSiteConfigResponse,
+ WebSocketJsonResponse,
+} from '../interfaces';
+import { WebSocketService } from '../services';
+import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
+import autosize from 'autosize';
+import { SiteForm } from './site-form';
+import { UserListing } from './user-listing';
+import { i18n } from '../i18next';
+
+interface AdminSettingsState {
+ siteRes: GetSiteResponse;
+ siteConfigRes: GetSiteConfigResponse;
+ siteConfigForm: SiteConfigForm;
+ loading: boolean;
+ siteConfigLoading: boolean;
+}
+
+export class AdminSettings extends Component<any, AdminSettingsState> {
+ private siteConfigTextAreaId = `site-config-${randomStr()}`;
+ private subscription: Subscription;
+ private emptyState: AdminSettingsState = {
+ siteRes: {
+ site: {
+ id: null,
+ name: null,
+ creator_id: null,
+ creator_name: null,
+ published: null,
+ number_of_users: null,
+ number_of_posts: null,
+ number_of_comments: null,
+ number_of_communities: null,
+ enable_downvotes: null,
+ open_registration: null,
+ enable_nsfw: null,
+ },
+ admins: [],
+ banned: [],
+ online: null,
+ },
+ siteConfigForm: {
+ config_hjson: null,
+ auth: null,
+ },
+ siteConfigRes: {
+ config_hjson: null,
+ },
+ loading: true,
+ siteConfigLoading: null,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ WebSocketService.Instance.getSiteConfig();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ render() {
+ return (
+ <div class="container">
+ {this.state.loading ? (
+ <h5>
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <div class="row">
+ <div class="col-12 col-md-6">
+ <SiteForm site={this.state.siteRes.site} />
+ {this.admins()}
+ {this.bannedUsers()}
+ </div>
+ <div class="col-12 col-md-6">{this.adminSettings()}</div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ admins() {
+ return (
+ <>
+ <h5>{capitalizeFirstLetter(i18n.t('admins'))}</h5>
+ <ul class="list-unstyled">
+ {this.state.siteRes.admins.map(admin => (
+ <li class="list-inline-item">
+ <UserListing
+ user={{
+ name: admin.name,
+ avatar: admin.avatar,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ </>
+ );
+ }
+
+ bannedUsers() {
+ return (
+ <>
+ <h5>{i18n.t('banned_users')}</h5>
+ <ul class="list-unstyled">
+ {this.state.siteRes.banned.map(banned => (
+ <li class="list-inline-item">
+ <UserListing
+ user={{
+ name: banned.name,
+ avatar: banned.avatar,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ </>
+ );
+ }
+
+ adminSettings() {
+ return (
+ <div>
+ <h5>{i18n.t('admin_settings')}</h5>
+ <form onSubmit={linkEvent(this, this.handleSiteConfigSubmit)}>
+ <div class="form-group row">
+ <label
+ class="col-12 col-form-label"
+ htmlFor={this.siteConfigTextAreaId}
+ >
+ {i18n.t('site_config')}
+ </label>
+ <div class="col-12">
+ <textarea
+ id={this.siteConfigTextAreaId}
+ value={this.state.siteConfigForm.config_hjson}
+ onInput={linkEvent(this, this.handleSiteConfigHjsonChange)}
+ class="form-control text-monospace"
+ rows={3}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-12">
+ <button type="submit" class="btn btn-secondary mr-2">
+ {this.state.siteConfigLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ capitalizeFirstLetter(i18n.t('save'))
+ )}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+
+ handleSiteConfigSubmit(i: AdminSettings, event: any) {
+ event.preventDefault();
+ i.state.siteConfigLoading = true;
+ WebSocketService.Instance.saveSiteConfig(i.state.siteConfigForm);
+ i.setState(i.state);
+ }
+
+ handleSiteConfigHjsonChange(i: AdminSettings, event: any) {
+ i.state.siteConfigForm.config_hjson = event.target.value;
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.context.router.history.push('/');
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else if (msg.reconnect) {
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+
+ // This means it hasn't been set up yet
+ if (!data.site) {
+ this.context.router.history.push('/setup');
+ }
+ this.state.siteRes = data;
+ this.setState(this.state);
+ document.title = `${i18n.t('admin_settings')} - ${
+ this.state.siteRes.site.name
+ }`;
+ } else if (res.op == UserOperation.EditSite) {
+ let data = res.data as SiteResponse;
+ this.state.siteRes.site = data.site;
+ this.setState(this.state);
+ toast(i18n.t('site_saved'));
+ } else if (res.op == UserOperation.GetSiteConfig) {
+ let data = res.data as GetSiteConfigResponse;
+ this.state.siteConfigRes = data;
+ this.state.loading = false;
+ this.state.siteConfigForm.config_hjson = this.state.siteConfigRes.config_hjson;
+ this.setState(this.state);
+ var textarea: any = document.getElementById(this.siteConfigTextAreaId);
+ autosize(textarea);
+ } else if (res.op == UserOperation.SaveSiteConfig) {
+ let data = res.data as GetSiteConfigResponse;
+ this.state.siteConfigRes = data;
+ this.state.siteConfigForm.config_hjson = this.state.siteConfigRes.config_hjson;
+ this.state.siteConfigLoading = false;
+ toast(i18n.t('site_saved'));
+ this.setState(this.state);
+ }
+ }
+}
diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx
index ae3e7cfc..b3c1a9a1 100644
--- a/ui/src/components/comment-form.tsx
+++ b/ui/src/components/comment-form.tsx
@@ -17,10 +17,12 @@ import {
toast,
setupTribute,
wsJsonToRes,
+ emojiPicker,
} from '../utils';
import { WebSocketService, UserService } from '../services';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
+import emojiShortName from 'emoji-short-name';
import { i18n } from '../i18next';
interface CommentFormProps {
@@ -69,6 +71,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
super(props, context);
this.tribute = setupTribute();
+ this.setupEmojiPicker();
+
this.state = this.emptyState;
if (this.props.node) {
@@ -158,8 +162,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
</button>
{this.state.commentForm.content && (
<button
- className={`btn btn-sm mr-2 btn-secondary ${this.state
- .previewMode && 'active'}`}
+ className={`btn btn-sm mr-2 btn-secondary ${
+ this.state.previewMode && 'active'
+ }`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
@@ -209,6 +214,15 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<use xlinkHref="#icon-spinner"></use>
</svg>
)}
+ <span
+ onClick={linkEvent(this, this.handleEmojiPickerClick)}
+ class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
+ data-tippy-content={i18n.t('emoji_picker')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-smile"></use>
+ </svg>
+ </span>
</div>
</div>
</form>
@@ -216,6 +230,20 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
);
}
+ setupEmojiPicker() {
+ emojiPicker.on('emoji', twemojiHtmlStr => {
+ if (this.state.commentForm.content == null) {
+ this.state.commentForm.content = '';
+ }
+ var el = document.createElement('div');
+ el.innerHTML = twemojiHtmlStr;
+ let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
+ let shortName = `:${emojiShortName[nativeUnicode]}:`;
+ this.state.commentForm.content += shortName;
+ this.setState(this.state);
+ });
+ }
+
handleFinished() {
this.state.previewMode = false;
this.state.loading = false;
@@ -242,6 +270,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
i.setState(i.state);
}
+ handleEmojiPickerClick(_i: CommentForm, event: any) {
+ emojiPicker.togglePicker(event.target);
+ }
+
handleCommentContentChange(i: CommentForm, event: any) {
i.state.commentForm.content = event.target.value;
i.setState(i.state);
diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx
index 39f29b5f..69a78f50 100644
--- a/ui/src/components/comment-node.tsx
+++ b/ui/src/components/comment-node.tsx
@@ -24,8 +24,6 @@ import {
getUnixTime,
canMod,
isMod,
- pictshareAvatarThumbnail,
- showAvatars,
setupTippy,
colorList,
} from '../utils';
@@ -33,6 +31,7 @@ import moment from 'moment';
import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes';
+import { UserListing } from './user-listing';
import { i18n } from '../i18next';
interface CommentNodeState {
@@ -143,25 +142,21 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
>
<div
- class={`${!this.props.noIndent &&
+ class={`${
+ !this.props.noIndent &&
this.props.node.comment.parent_id &&
- 'ml-2'}`}
+ 'ml-2'
+ }`}
>
<div class="d-flex flex-wrap align-items-center mb-1 mt-1 text-muted small">
- <Link
- className="mr-2 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>
+ <span class="mr-2">
+ <UserListing
+ user={{
+ name: node.comment.creator_name,
+ avatar: node.comment.creator_avatar,
+ }}
+ />
+ </span>
{this.isMod && (
<div className="badge badge-light d-none d-sm-inline mr-2">
{i18n.t('mod')}
@@ -191,7 +186,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</>
)}
<div
- className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mr-2"
+ className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
onClick={linkEvent(this, this.handleCommentCollapse)}
>
{this.state.collapsed ? (
@@ -256,8 +251,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.loadingIcon
) : (
<svg
- class={`icon icon-inline ${node.comment.read &&
- 'text-success'}`}
+ class={`icon icon-inline ${
+ node.comment.read && 'text-success'
+ }`}
>
<use xlinkHref="#icon-check"></use>
</svg>
@@ -309,8 +305,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.loadingIcon
) : (
<svg
- class={`icon icon-inline ${node.comment.saved &&
- 'text-warning'}`}
+ class={`icon icon-inline ${
+ node.comment.saved && 'text-warning'
+ }`}
>
<use xlinkHref="#icon-star"></use>
</svg>
@@ -357,8 +354,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
data-tippy-content={i18n.t('view_source')}
>
<svg
- class={`icon icon-inline ${this.state
- .viewSource && 'text-success'}`}
+ class={`icon icon-inline ${
+ this.state.viewSource && 'text-success'
+ }`}
>
<use xlinkHref="#icon-file-text"></use>
</svg>
@@ -387,8 +385,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
>
<svg
- class={`icon icon-inline ${node.comment
- .deleted && 'text-danger'}`}
+ class={`icon icon-inline ${
+ node.comment.deleted && 'text-danger'
+ }`}
>
<use xlinkHref="#icon-trash"></use>
</svg>
diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx
index 38003312..366d3be8 100644
--- a/ui/src/components/main.tsx
+++ b/ui/src/components/main.tsx
@@ -33,13 +33,12 @@ import { SortSelect } from './sort-select';
import { ListingTypeSelect } from './listing-type-select';
import { DataTypeSelect } from './data-type-select';
import { SiteForm } from './site-form';
+import { UserListing } from './user-listing';
import {
wsJsonToRes,
repoUrl,
mdToHtml,
fetchLimit,
- pictshareAvatarThumbnail,
- showAvatars,
toast,
getListingTypeFromProps,
getPageFromProps,
@@ -316,20 +315,12 @@ export class Main extends Component<any, MainState> {
<li class="list-inline-item">{i18n.t('admins')}:</li>
{this.state.siteRes.admins.map(admin => (
<li class="list-inline-item">
- <Link
- class="text-body font-weight-bold"
- to={`/u/${admin.name}`}
- >
- {admin.avatar && showAvatars() && (
- <img
- height="32"
- width="32"
- src={pictshareAvatarThumbnail(admin.avatar)}
- class="rounded-circle mr-1"
- />
- )}
- <span>{admin.name}</span>
- </Link>
+ <UserListing
+ user={{
+ name: admin.name,
+ avatar: admin.avatar,
+ }}
+ />
</li>
))}
</ul>
@@ -619,6 +610,7 @@ export class Main extends Component<any, MainState> {
this.state.siteRes.site = data.site;
this.state.showEditSite = false;
this.setState(this.state);
+ toast(i18n.t('site_saved'));
} else if (res.op == UserOperation.GetPosts) {
let data = res.data as GetPostsResponse;
this.state.posts = data.posts;
diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx
index d7f3b5a8..f1936be1 100644
--- a/ui/src/components/navbar.tsx
+++ b/ui/src/components/navbar.tsx
@@ -16,6 +16,7 @@ import {
Comment,
CommentResponse,
PrivateMessage,
+ UserView,
PrivateMessageResponse,
WebSocketJsonResponse,
} from '../interfaces';
@@ -40,6 +41,7 @@ interface NavbarState {
messages: Array<PrivateMessage>;
unreadCount: number;
siteName: string;
+ admins: Array<UserView>;
}
export class Navbar extends Component<any, NavbarState> {
@@ -53,6 +55,7 @@ export class Navbar extends Component<any, NavbarState> {
messages: [],
expanded: false,
siteName: undefined,
+ admins: [],
};
constructor(props: any, context: any) {
@@ -179,6 +182,19 @@ export class Navbar extends Component<any, NavbarState> {
</li>
</ul>
<ul class="navbar-nav ml-auto">
+ {this.canAdmin && (
+ <li className="nav-item mt-1">
+ <Link
+ class="nav-link"
+ to={`/admin`}
+ title={i18n.t('admin_settings')}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-settings"></use>
+ </svg>
+ </Link>
+ </li>
+ )}
{this.state.isLoggedIn ? (
<>
<li className="nav-item mt-1">
@@ -298,7 +314,10 @@ export class Navbar extends Component<any, NavbarState> {
if (data.site && !this.state.siteName) {
this.state.siteName = data.site.name;
+ this.state.admins = data.admins;
WebSocketService.Instance.site = data.site;
+ WebSocketService.Instance.admins = data.admins;
+
this.setState(this.state);
}
}
@@ -353,9 +372,16 @@ export class Navbar extends Component<any, NavbarState> {
);
}
+ get canAdmin(): boolean {
+ return (
+ UserService.Instance.user &&
+ this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
+ );
+ }
+
requestNotificationPermission() {
if (UserService.Instance.user) {
- document.addEventListener('DOMContentLoaded', function() {
+ document.addEventListener('DOMContentLoaded', function () {
if (!Notification) {
toast(i18n.t('notifications_error'), 'danger');
return;
diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx
index 47920b9b..4dbc8b23 100644
--- a/ui/src/components/post-form.tsx
+++ b/ui/src/components/post-form.tsx
@@ -34,9 +34,11 @@ import {
randomStr,
setupTribute,
setupTippy,
+ emojiPicker,
} from '../utils';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
+import emojiShortName from 'emoji-short-name';
import Selectr from 'mobius1-selectr';
import { i18n } from '../i18next';
@@ -92,6 +94,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
this.tribute = setupTribute();
+ this.setupEmojiPicker();
+
this.state = this.emptyState;
if (this.props.post) {
@@ -190,8 +194,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<form>
<label
htmlFor="file-upload"
- className={`${UserService.Instance.user &&
- 'pointer'} d-inline-block float-right text-muted h6 font-weight-bold`}
+ className={`${
+ UserService.Instance.user && 'pointer'
+ } d-inline-block float-right text-muted font-weight-bold`}
data-tippy-content={i18n.t('upload_image')}
>
<svg class="icon icon-inline">
@@ -284,8 +289,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
)}
{this.state.postForm.body && (
<button
- className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
- .previewMode && 'active'}`}
+ className={`mt-1 mr-2 btn btn-sm btn-secondary ${
+ this.state.previewMode && 'active'
+ }`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
@@ -294,13 +300,22 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<a
href={markdownHelpUrl}
target="_blank"
- class="d-inline-block float-right text-muted h6 font-weight-bold"
+ class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
+ <span
+ onClick={linkEvent(this, this.handleEmojiPickerClick)}
+ class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
+ data-tippy-content={i18n.t('emoji_picker')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-smile"></use>
+ </svg>
+ </span>
</div>
</div>
{!this.props.post && (
@@ -369,6 +384,20 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
);
}
+ setupEmojiPicker() {
+ emojiPicker.on('emoji', twemojiHtmlStr => {
+ if (this.state.postForm.body == null) {
+ this.state.postForm.body = '';
+ }
+ var el = document.createElement('div');
+ el.innerHTML = twemojiHtmlStr;
+ let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
+ let shortName = `:${emojiShortName[nativeUnicode]}:`;
+ this.state.postForm.body += shortName;
+ this.setState(this.state);
+ });
+ }
+
handlePostSubmit(i: PostForm, event: any) {
event.preventDefault();
if (i.props.post) {
@@ -512,6 +541,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
});
}
+ handleEmojiPickerClick(_i: PostForm, event: any) {
+ emojiPicker.togglePicker(event.target);
+ }
+
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (msg.error) {
diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx
index 49970dfc..49749201 100644
--- a/ui/src/components/post-listing.tsx
+++ b/ui/src/components/post-listing.tsx
@@ -19,18 +19,19 @@ import {
import { MomentTime } from './moment-time';
import { PostForm } from './post-form';
import { IFramelyCard } from './iframely-card';
+import { UserListing } from './user-listing';
import {
+ md,
mdToHtml,
canMod,
isMod,
isImage,
isVideo,
getUnixTime,
- pictshareAvatarThumbnail,
- showAvatars,
pictshareImage,
setupTippy,
hostname,
+ previewLines,
} from '../utils';
import { i18n } from '../i18next';
@@ -415,20 +416,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<ul class="list-inline mb-0 text-muted small">
<li className="list-inline-item">
<span>{i18n.t('by')} </span>
- <Link
- className="text-body font-weight-bold"
- to={`/u/${post.creator_name}`}
- >
- {post.creator_avatar && showAvatars() && (
- <img
- he