diff options
Diffstat (limited to 'ui')
-rw-r--r-- | ui/src/components/admin-settings.tsx | 241 | ||||
-rw-r--r-- | ui/src/components/comment-node.tsx | 27 | ||||
-rw-r--r-- | ui/src/components/main.tsx | 24 | ||||
-rw-r--r-- | ui/src/components/navbar.tsx | 26 | ||||
-rw-r--r-- | ui/src/components/post-listing.tsx | 23 | ||||
-rw-r--r-- | ui/src/components/private-message-form.tsx | 25 | ||||
-rw-r--r-- | ui/src/components/private-message.tsx | 1 | ||||
-rw-r--r-- | ui/src/components/search.tsx | 23 | ||||
-rw-r--r-- | ui/src/components/sidebar.tsx | 21 | ||||
-rw-r--r-- | ui/src/components/site-form.tsx | 7 | ||||
-rw-r--r-- | ui/src/components/symbols.tsx | 3 | ||||
-rw-r--r-- | ui/src/components/user-listing.tsx | 36 | ||||
-rw-r--r-- | ui/src/index.tsx | 116 | ||||
-rw-r--r-- | ui/src/interfaces.ts | 22 | ||||
-rw-r--r-- | ui/src/services/WebSocketService.ts | 13 | ||||
-rw-r--r-- | ui/translations/en.json | 4 |
16 files changed, 457 insertions, 155 deletions
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-node.tsx b/ui/src/components/comment-node.tsx index 39f29b5f..ba4301e1 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 { @@ -148,20 +147,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { '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 +184,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 ? ( 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..e0d8aff5 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,6 +372,13 @@ 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() { diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 101d1807..d0efa043 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -19,6 +19,7 @@ import { import { MomentTime } from './moment-time'; import { PostForm } from './post-form'; import { IFramelyCard } from './iframely-card'; +import { UserListing } from './user-listing'; import { md, mdToHtml, @@ -27,8 +28,6 @@ import { isImage, isVideo, getUnixTime, - pictshareAvatarThumbnail, - showAvatars, pictshareImage, setupTippy, previewLines, @@ -417,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 - height="32" - width="32" - src={pictshareAvatarThumbnail(post.creator_avatar)} - class="rounded-circle mr-1" - /> - )} - <span>{post.creator_name}</span> - </Link> + <UserListing + user={{ + name: post.creator_name, + avatar: post.creator_avatar, + }} + /> {this.isMod && ( <span className="mx-1 badge badge-light"> {i18n.t('mod')} diff --git a/ui/src/components/private-message-form.tsx b/ui/src/components/private-message-form.tsx index 7e498bae..6b607654 100644 --- a/ui/src/components/private-message-form.tsx +++ b/ui/src/components/private-message-form.tsx @@ -21,14 +21,13 @@ import { capitalizeFirstLetter, markdownHelpUrl, mdToHtml, - showAvatars, - pictshareAvatarThumbnail, wsJsonToRes, toast, randomStr, setupTribute, setupTippy, } from '../utils'; +import { UserListing } from './user-listing'; import Tribute from 'tributejs/src/Tribute.js'; import autosize from 'autosize'; import { i18n } from '../i18next'; @@ -132,22 +131,12 @@ export class PrivateMessageForm extends Component< {this.state.recipient && ( <div class="col-sm-10 form-control-plaintext"> - <Link - className="text-body font-weight-bold" - to={`/u/${this.state.recipient.name}`} - > - {this.state.recipient.avatar && showAvatars() && ( - <img - height="32" - width="32" - src={pictshareAvatarThumbnail( - this.state.recipient.avatar - )} - class="rounded-circle mr-1" - /> - )} - <span>{this.state.recipient.name}</span> - </Link> + <UserListing + user={{ + name: this.state.recipient.name, + avatar: this.state.recipient.avatar, + }} + /> </div> )} </div> diff --git a/ui/src/components/private-message.tsx b/ui/src/components/private-message.tsx index ef128dd4..337b1650 100644 --- a/ui/src/components/private-message.tsx +++ b/ui/src/components/private-message.tsx @@ -58,6 +58,7 @@ export class PrivateMessage extends Component< <div class="border-top border-light"> <div> <ul class="list-inline mb-0 text-muted small"> + {/* TODO refactor this */} <li className="list-inline-item"> {this.mine ? i18n.t('to') : i18n.t('from')} </li> diff --git a/ui/src/components/search.tsx b/ui/src/components/search.tsx index 3fd2f467..b9662fae 100644 --- a/ui/src/components/search.tsx +++ b/ui/src/components/search.tsx @@ -30,6 +30,7 @@ import { commentsToFlatNodes, } from '../utils'; import { PostListing } from './post-listing'; +import { UserListing } from './user-listing'; import { SortSelect } from './sort-select'; import { CommentNodes } from './comment-nodes'; import { i18n } from '../i18next'; @@ -266,22 +267,12 @@ export class Search extends Component<any, SearchState> { {i.type_ == 'users' && ( <div> <span> - <Link - className="text-info" - to={`/u/${(i.data as UserView).name}`} - > - {(i.data as UserView).avatar && showAvatars() && ( - <img - height="32" - width="32" - src={pictshareAvatarThumbnail( - (i.data as UserView).avatar - )} - class="rounded-circle mr-1" - /> - )} - <span>{`/u/${(i.data as UserView).name}`}</span> - </Link> + <UserListing + user={{ + name: (i.data as UserView).name, + avatar: (i.data as UserView).avatar, + }} + /> </span> <span>{` - ${ (i.data as UserView).comment_score diff --git a/ui/src/components/sidebar.tsx b/ui/src/components/sidebar.tsx index 0f4a0e10..d66266f6 100644 --- a/ui/src/components/sidebar.tsx +++ b/ui/src/components/sidebar.tsx @@ -15,6 +15,7 @@ import { showAvatars, } from '../utils'; import { CommunityForm } from './community-form'; +import { UserListing } from './user-listing'; import { i18n } from '../i18next'; interface SidebarProps { @@ -204,20 +205,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> { <li class="list-inline-item">{i18n.t('mods')}: </li> {this.props.moderators.map(mod => ( <li class="list-inline-item"> - <Link - class="text-body font-weight-bold" - to={`/u/${mod.user_name}`} - > - {mod.avatar && showAvatars() && ( - <img - height="32" - width="32" - src={pictshareAvatarThumbnail(mod.avatar)} - class="rounded-circle mr-1" - /> - )} - <span>{mod.user_name}</span> - </Link> + <UserListing + user={{ + name: mod.user_name, + avatar: mod.avatar, + }} + /> </li> ))} </ul> diff --git a/ui/src/components/site-form.tsx b/ui/src/components/site-form.tsx index df913043..f0c80585 100644 --- a/ui/src/components/site-form.tsx +++ b/ui/src/components/site-form.tsx @@ -58,12 +58,19 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> { }); } + // Necessary to stop the loading + componentWillReceiveProps() { + this.state.loading = false; + this.setState(this.state); + } + render() { return ( <> <Prompt when={ !this.state.loading && + !this.props.site && (this.state.siteForm.name || this.state.siteForm.description) } message={i18n.t('block_leaving')} diff --git a/ui/src/components/symbols.tsx b/ui/src/components/symbols.tsx index dae734a8..87ba879e 100644 --- a/ui/src/components/symbols.tsx +++ b/ui/src/components/symbols.tsx @@ -15,6 +15,9 @@ export class Symbols extends Component<any, any> { xmlnsXlink="http://www.w3.org/1999/xlink" > <defs> + <symbol id="icon-settings" viewBox="0 0 24 24"> + <path d="M16 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM14 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414zM20.315 15.404c0.046-0.105 0.112-0.191 0.192-0.257 0.112-0.092 0.251-0.146 0.403-0.147h0.090c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121-0.337-1.58-0.879-2.121-1.293-0.879-2.121-0.879h-0.159c-0.11-0.001-0.215-0.028-0.308-0.076-0.127-0.066-0.23-0.172-0.292-0.312-0.003-0.029-0.004-0.059-0.004-0.089-0.024-0.055-0.040-0.111-0.049-0.168 0.020-0.334 0.077-0.454 0.168-0.547l0.062-0.062c0.585-0.586 0.878-1.356 0.877-2.122s-0.294-1.536-0.881-2.122c-0.586-0.585-1.356-0.878-2.122-0.877s-1.536 0.294-2.12 0.879l-0.046 0.046c-0.083 0.080-0.183 0.136-0.288 0.166-0.14 0.039-0.291 0.032-0.438-0.033-0.101-0.044-0.187-0.11-0.253-0.19-0.092-0.112-0.146-0.251-0.147-0.403v-0.090c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879-1.58 0.337-2.121 0.879-0.879 1.293-0.879 2.121v0.159c-0.001 0.11-0.028 0.215-0.076 0.308-0.066 0.127-0.172 0.23-0.312 0.292-0.029 0.003-0.059 0.004-0.089 0.004-0.055 0.024-0.111 0.040-0.168 0.049-0.335-0.021-0.455-0.078-0.548-0.169l-0.062-0.062c-0.586-0.585-1.355-0.878-2.122-0.878s-1.535 0.294-2.122 0.882c-0.585 0.586-0.878 1.355-0.878 2.122s0.294 1.536 0.879 2.12l0.048 0.047c0.080 0.083 0.136 0.183 0.166 0.288 0.039 0.14 0.032 0.291-0.031 0.434-0.006 0.016-0.013 0.034-0.021 0.052-0.041 0.109-0.108 0.203-0.191 0.275-0.11 0.095-0.25 0.153-0.383 0.156h-0.090c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.294-0.879 2.122 0.337 1.58 0.879 2.121 1.293 0.879 2.121 0.879h0.159c0.11 0.001 0.215 0.028 0.308 0.076 0.128 0.067 0.233 0.174 0.296 0.321 0.024 0.055 0.040 0.111 0.049 0.168-0.020 0.334-0.077 0.454-0.168 0.547l-0.062 0.062c-0.585 0.586-0.878 1.356-0.877 2.122s0.294 1.536 0.881 2.122c0.586 0.585 1.356 0.878 2.122 0.877s1.536-0.294 2.12-0.879l0.047-0.048c0.083-0.080 0.183-0.136 0.288-0.166 0.14-0.039 0.291-0.032 0.434 0.031 0.016 0.006 0.034 0.013 0.052 0.021 0.109 0.041 0.203 0.108 0.275 0.191 0.095 0.11 0.153 0.25 0.156 0.383v0.092c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879 1.58-0.337 2.121-0.879 0.879-1.293 0.879-2.121v-0.159c0.001-0.11 0.028-0.215 0.076-0.308 0.067-0.128 0.174-0.233 0.321-0.296 0.055-0.024 0.111-0.040 0.168-0.049 0.334 0.020 0.454 0.077 0.547 0.168l0.062 0.062c0.586 0.585 1.356 0.878 2.122 0.877s1.536-0.294 2.122-0.881c0.585-0.586 0.878-1.356 0.877-2.122s-0.294-1.536-0.879-2.12l-0.048-0.047c-0.080-0.083-0.136-0.183-0.166-0.288-0.039-0.14-0.032-0.291 0.031-0.434zM18.396 9.302c-0.012-0.201-0.038-0.297-0.076-0.382v0.080c0 0.043 0.003 0.084 0.008 0.125 0.021 0.060 0.043 0.119 0.068 0.177 0.004 0.090 0.005 0.091 0.005 0.092 0.249 0.581 0.684 1.030 1.208 1.303 0.371 0.193 0.785 0.298 1.211 0.303h0.18c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707-0.111 0.525-0.293 0.707-0.431 0.293-0.707 0.293h-0.090c-0.637 0.003-1.22 0.228-1.675 0.603-0.323 0.266-0.581 0.607-0.75 0.993-0.257 0.582-0.288 1.21-0.127 1.782 0.119 0.423 0.341 0.814 0.652 1.136l0.072 0.073c0.196 0.196 0.294 0.45 0.294 0.707s-0.097 0.512-0.292 0.707c-0.197 0.197-0.451 0.295-0.709 0.295s-0.512-0.097-0.707-0.292l-0.061-0.061c-0.463-0.453-1.040-0.702-1.632-0.752-0.437-0.037-0.882 0.034-1.293 0.212-0.578 0.248-1.027 0.683-1.3 1.206-0.193 0.371-0.298 0.785-0.303 1.211v0.181c0 0.276-0.111 0.525-0.293 0.707s-0.43 0.292-0.706 0.292-0.525-0.111-0.707-0.293-0.293-0.431-0.293-0.707v-0.090c-0.015-0.66-0.255-1.242-0.644-1.692-0.284-0.328-0.646-0.585-1.058-0.744-0.575-0.247-1.193-0.274-1.756-0.116-0.423 0.119-0.814 0.341-1.136 0.652l-0.073 0.072c-0.196 0.196-0.45 0.294-0.707 0.294s-0.512-0.097-0.707-0.292c-0.197-0.197-0.295-0.451-0.295-0.709s0.097-0.512 0.292-0.707l0.061-0.061c0.453-0.463 0.702-1.040 0.752-1.632 0.037-0.437-0.034-0.882-0.212-1.293-0.248-0.578-0.683-1.027-1.206-1.3-0.371-0.193-0.785-0.298-1.211-0.303l-0.18 0.001c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707 0.111-0.525 0.293-0.707 0.431-0.293 0.707-0.293h0.090c0.66-0.015 1.242-0.255 1.692-0.644 0.328-0.284 0.585-0.646 0.744-1.058 0.247-0.575 0.274-1.193 0.116-1.756-0.119-0.423-0.341-0.814-0.652-1.136l-0.073-0.073c-0.196-0.196-0.294-0.45-0.294-0.707s0.097-0.512 0.292-0.707c0.197-0.197 0.451-0.295 0.709-0.295s0.512 0.097 0.707 0.292l0.061 0.061c0.463 0.453 1.040 0.702 1.632 0.752 0.37 0.032 0.745-0.014 1.101-0.137 0.096-0.012 0.186-0.036 0.266-0.072-0.031 0.001-0.061 0.003-0.089 0.004-0.201 0.012-0.297 0.038-0.382 0.076h0.080c0.043 0 0.084-0.003 0.125-0.008 0.060-0.021 0.119-0.043 0.177-0.068 0.090-0.004 0.091-0.005 0.092-0.005 0.581-0.249 1.030-0.684 1.303-1.208 0.193-0.37 0.298-0.785 0.303-1.21v-0.181c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293 0.525 0.111 0.707 0.293 0.293 0.431 0.293 0.707v0.090c0.003 0.637 0.228 1.22 0.603 1.675 0.266 0.323 0.607 0.581 0.996 0.751 0.578 0.255 1.206 0.286 1.778 0.125 0.423-0.119 0.814-0.341 1.136-0.652l0.073-0.072c0.196-0.196 0.45-0.294 0.707-0.294s0.512 0.097 0.707 0.292c0.197 0.197 0.295 0.451 0.295 0.709s-0.097 0.512-0.292 0.707l-0.061 0.061c-0.453 0.463-0.702 1.040-0.752 1.632-0.032 0.37 0.014 0.745 0.137 1.101 0.012 0.095 0.037 0.185 0.072 0.266-0.001-0.032-0.002-0.062-0.004-0.089z"></path> + </symbol> <symbol id="icon-book-open" viewBox="0 0 24 24"> <path d="M21 4v13h-6c-0.728 0-1.412 0.195-2 0.535v-10.535c0-0.829 0.335-1.577 0.879-2.121s1.292-0.879 2.121-0.879zM11 17.535c-0.588-0.34-1.272-0.535-2-0.535h-6v-13h5c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121zM22 2h-6c-1.38 0-2.632 0.561-3.536 1.464-0.167 0.167-0.322 0.346-0.464 0.536-0.142-0.19-0.297-0.369-0.464-0.536-0.904-0.903-2.156-1.464-3.536-1.464h-6c-0.552 0-1 0.448-1 1v15c0 0.552 0.448 1 1 1h7c0.553 0 1.051 0.223 1.414 0.586s0.586 0.861 0.586 1.414c0 0.552 0.448 1 1 1s1-0.448 1-1c0-0.553 0.223-1.051 0.586-1.414s0.861-0.586 1.414-0.586h7c0.552 0 1-0.448 1-1v-15c0-0.552-0.448-1-1-1z"></path> </symbol> diff --git a/ui/src/components/user-listing.tsx b/ui/src/components/user-listing.tsx new file mode 100644 index 00000000..1f136e00 --- /dev/null +++ b/ui/src/components/user-listing.tsx @@ -0,0 +1,36 @@ +import { Component } from 'inferno'; +import { Link } from 'inferno-router'; +import { UserView } from '../interfaces'; +import { pictshareAvatarThumbnail, showAvatars } from '../utils'; + +interface UserOther { + name: string; + avatar?: string; +} + +interface UserListingProps { + user: UserView | UserOther; +} + +export class UserListing extends Component<UserListingProps, any> { + constructor(props: any, context: any) { + super(props, context); + } + + render() { + let user = this.props.user; + return ( + <Link className="text-body font-weight-bold" to={`/u/${user.name}`}> + {user.avatar && showAvatars() && ( + <img + height="32" + width="32" + src={pictshareAvatarThumbnail(user.avatar)} + class="rounded-circle mr-2" + /> + )} + <span>{user.name}</span> + </Link> + ); + } +} diff --git a/ui/src/index.tsx b/ui/src/index.tsx index c56f6c4e..8e49df9f 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -15,79 +15,85 @@ import { Communities } from './components/communities'; import { User } from './components/user'; import { Modlog } from './components/modlog'; import { Setup } from './components/setup'; +import { AdminSettings } from './components/admin-settings'; import { Inbox } from './components/inbox'; import { Search } from './components/search'; import { Sponsors } from './components/sponsors'; import { Symbols } from './components/symbols'; import { i18n } from './i18next'; -import { WebSocketService, UserService } from './services'; - const container = document.getElementById('app'); class Index extends Component<any, any> { constructor(props: any, context: any) { super(props, context); - WebSocketService.Instance; - UserService.Instance; } render() { return ( <Provider i18next={i18n}> <BrowserRouter> - <Navbar /> - <div class="mt-4 p-0 fl-1"> - <Switch> - <Route exact path={`/`} component={Main} /> - <Route - path={`/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`} - component={Main} - /> - <Route path={`/login`} component={Login} /> - <Route path={`/create_post`} component={CreatePost} /> - <Route path={`/create_community`} component={CreateCommunity} /> - <Route - path={`/create_private_message`} - component={CreatePrivateMessage} - /> - <Route path={`/communities/page/:page`} component={Communities} /> - <Route path={`/communities`} component={Communities} /> - <Route path={`/post/:id/comment/:comment_id`} component={Post} /> - <Route path={`/post/:id`} component={Post} /> - <Route - path={`/c/:name/data_type/:data_type/sort/:sort/page/:page`} - component={Community} - /> - <Route path={`/community/:id`} component={Community} /> - <Route path={`/c/:name`} component={Community} /> - <Route - path={`/u/:username/view/:view/sort/:sort/page/:page`} - component={User} - /> - <Route path={`/user/:id`} component={User} /> - <Route path={`/u/:username`} component={User} /> - <Route path={`/inbox`} component={Inbox} /> - <Route - path={`/modlog/community/:community_id`} - component={Modlog} - /> - <Route path={`/modlog`} component={Modlog} /> - <Route path={`/setup`} component={Setup} /> - <Route - path={`/search/q/:q/type/:type/sort/:sort/page/:page`} - component={Search} - /> - <Route path={`/search`} component={Search} /> - <Route path={`/sponsors`} component={Sponsors} /> - <Route - path={`/password_change/:token`} - component={PasswordChange} - /> - </Switch> - <Symbols /> + <div> + <Navbar /> + <div class="mt-4 p-0 fl-1"> + <Switch> + <Route exact path={`/`} component={Main} /> + <Route + path={`/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`} + component={Main} + /> + <Route path={`/login`} component={Login} /> + <Route path={`/create_post`} component={CreatePost} /> + <Route path={`/create_community`} component={CreateCommunity} /> + <Route + path={`/create_private_message`} |