summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDessalines <tyhou13@gmx.com>2020-04-10 16:55:57 -0400
committerDessalines <tyhou13@gmx.com>2020-04-10 16:55:57 -0400
commitbb287cbd076940bd09f6afb61b642370d020f91e (patch)
treef3fe3afa29b6445fbb2232a8c5a9133b8d691a91
parented264aba3c12243352f68c2de6a5f21f23778bd0 (diff)
Adding an admin settings page.
- Fixes #620 - Adding a UserListing component. Fixes #627
-rw-r--r--docker/dev/docker-compose.yml2
-rw-r--r--docker/prod/docker-compose.yml2
-rw-r--r--docs/src/contributing_websocket_http_api.md99
-rw-r--r--server/src/api/site.rs70
-rw-r--r--server/src/routes/api.rs2
-rw-r--r--server/src/routes/index.rs1
-rw-r--r--server/src/settings.rs12
-rw-r--r--server/src/websocket/mod.rs2
-rw-r--r--server/src/websocket/server.rs10
-rw-r--r--ui/src/components/admin-settings.tsx241
-rw-r--r--ui/src/components/comment-node.tsx27
-rw-r--r--ui/src/components/main.tsx24
-rw-r--r--ui/src/components/navbar.tsx26
-rw-r--r--ui/src/components/post-listing.tsx23
-rw-r--r--ui/src/components/private-message-form.tsx25
-rw-r--r--ui/src/components/private-message.tsx1
-rw-r--r--ui/src/components/search.tsx23
-rw-r--r--ui/src/components/sidebar.tsx21
-rw-r--r--ui/src/components/site-form.tsx7
-rw-r--r--ui/src/components/symbols.tsx3
-rw-r--r--ui/src/components/user-listing.tsx36
-rw-r--r--ui/src/index.tsx116
-rw-r--r--ui/src/interfaces.ts22
-rw-r--r--ui/src/services/WebSocketService.ts13
-rw-r--r--ui/translations/en.json4
25 files changed, 633 insertions, 179 deletions
diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml
index a7d289b2..3c52d1e5 100644
--- a/docker/dev/docker-compose.yml
+++ b/docker/dev/docker-compose.yml
@@ -21,7 +21,7 @@ services:
environment:
- RUST_LOG=debug
volumes:
- - ../lemmy.hjson:/config/config.hjson:ro
+ - ../lemmy.hjson:/config/config.hjson
depends_on:
- postgres
- pictshare
diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml
index 325effa6..a1b36162 100644
--- a/docker/prod/docker-compose.yml
+++ b/docker/prod/docker-compose.yml
@@ -19,7 +19,7 @@ services:
environment:
- RUST_LOG=error
volumes:
- - ./lemmy.hjson:/config/config.hjson:ro
+ - ./lemmy.hjson:/config/config.hjson
depends_on:
- postgres
- pictshare
diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md
index a73a1c13..f228f94e 100644
--- a/docs/src/contributing_websocket_http_api.md
+++ b/docs/src/contributing_websocket_http_api.md
@@ -92,85 +92,93 @@
- [Request](#request-17)
- [Response](#response-17)
- [HTTP](#http-18)
- * [Community](#community)
- + [Get Community](#get-community)
+ + [Get Site Config](#get-site-config)
- [Request](#request-18)
- [Response](#response-18)
- [HTTP](#http-19)
- + [Create Community](#create-community)
+ + [Save Site Config](#save-site-config)
- [Request](#request-19)
- [Response](#response-19)
- [HTTP](#http-20)
- + [List Communities](#list-communities)
+ * [Community](#community)
+ + [Get Community](#get-community)
- [Request](#request-20)
- [Response](#response-20)
- [HTTP](#http-21)
- + [Ban from Community](#ban-from-community)
+ + [Create Community](#create-community)
- [Request](#request-21)
- [Response](#response-21)
- [HTTP](#http-22)
- + [Add Mod to Community](#add-mod-to-community)
+ + [List Communities](#list-communities)
- [Request](#request-22)
- [Response](#response-22)
- [HTTP](#http-23)
- + [Edit Community](#edit-community)
+ + [Ban from Community](#ban-from-community)
- [Request](#request-23)
- [Response](#response-23)
- [HTTP](#http-24)
- + [Follow Community](#follow-community)
+ + [Add Mod to Community](#add-mod-to-community)
- [Request](#request-24)
- [Response](#response-24)
- [HTTP](#http-25)
- + [Get Followed Communities](#get-followed-communities)
+ + [Edit Community](#edit-community)
- [Request](#request-25)
- [Response](#response-25)
- [HTTP](#http-26)
- + [Transfer Community](#transfer-community)
+ + [Follow Community](#follow-community)
- [Request](#request-26)
- [Response](#response-26)
- [HTTP](#http-27)
- * [Post](#post)
- + [Create Post](#create-post)
+ + [Get Followed Communities](#get-followed-communities)
- [Request](#request-27)
- [Response](#response-27)
- [HTTP](#http-28)
- + [Get Post](#get-post)
+ + [Transfer Community](#transfer-community)
- [Request](#request-28)
- [Response](#response-28)
- [HTTP](#http-29)
- + [Get Posts](#get-posts)
+ * [Post](#post)
+ + [Create Post](#create-post)
- [Request](#request-29)
- [Response](#response-29)
- [HTTP](#http-30)
- + [Create Post Like](#create-post-like)
+ + [Get Post](#get-post)
- [Request](#request-30)
- [Response](#response-30)
- [HTTP](#http-31)
- + [Edit Post](#edit-post)
+ + [Get Posts](#get-posts)
- [Request](#request-31)
- [Response](#response-31)
- [HTTP](#http-32)
- + [Save Post](#save-post)
+ + [Create Post Like](#create-post-like)
- [Request](#request-32)
- [Response](#response-32)
- [HTTP](#http-33)
- * [Comment](#comment)
- + [Create Comment](#create-comment)
+ + [Edit Post](#edit-post)
- [Request](#request-33)
- [Response](#response-33)
- [HTTP](#http-34)
- + [Edit Comment](#edit-comment)
+ + [Save Post](#save-post)
- [Request](#request-34)
- [Response](#response-34)
- [HTTP](#http-35)
- + [Save Comment](#save-comment)
+ * [Comment](#comment)
+ + [Create Comment](#create-comment)
- [Request](#request-35)
- [Response](#response-35)
- [HTTP](#http-36)
- + [Create Comment Like](#create-comment-like)
+ + [Edit Comment](#edit-comment)
- [Request](#request-36)
- [Response](#response-36)
- [HTTP](#http-37)
+ + [Save Comment](#save-comment)
+ - [Request](#request-37)
+ - [Response](#response-37)
+ - [HTTP](#http-38)
+ + [Create Comment Like](#create-comment-like)
+ - [Request](#request-38)
+ - [Response](#response-38)
+ - [HTTP](#http-39)
* [RSS / Atom feeds](#rss--atom-feeds)
+ [All](#all)
+ [Community](#community-1)
@@ -779,6 +787,53 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
`POST /site/transfer`
+#### Get Site Config
+##### Request
+```rust
+{
+ op: "GetSiteConfig",
+ data: {
+ auth: String
+ }
+}
+```
+##### Response
+```rust
+{
+ op: "GetSiteConfig",
+ data: {
+ config_hjson: String,
+ }
+}
+```
+##### HTTP
+
+`GET /site/config`
+
+#### Save Site Config
+##### Request
+```rust
+{
+ op: "SaveSiteConfig",
+ data: {
+ config_hjson: String,
+ auth: String
+ }
+}
+```
+##### Response
+```rust
+{
+ op: "SaveSiteConfig",
+ data: {
+ config_hjson: String,
+ }
+}
+```
+##### HTTP
+
+`PUT /site/config`
+
### Community
#### Get Community
##### Request
diff --git a/server/src/api/site.rs b/server/src/api/site.rs
index 6bd90149..3720a2c4 100644
--- a/server/src/api/site.rs
+++ b/server/src/api/site.rs
@@ -97,6 +97,22 @@ pub struct TransferSite {
auth: String,
}
+#[derive(Serialize, Deserialize)]
+pub struct GetSiteConfig {
+ auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetSiteConfigResponse {
+ config_hjson: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct SaveSiteConfig {
+ config_hjson: String,
+ auth: String,
+}
+
impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
fn perform(&self, conn: &PgConnection) -> Result<ListCategoriesResponse, Error> {
let _data: &ListCategories = &self.data;
@@ -510,3 +526,57 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
})
}
}
+
+impl Perform<GetSiteConfigResponse> for Oper<GetSiteConfig> {
+ fn perform(&self, conn: &PgConnection) -> Result<GetSiteConfigResponse, Error> {
+ let data: &GetSiteConfig = &self.data;
+
+ let claims = match Claims::decode(&data.auth) {
+ Ok(claims) => claims.claims,
+ Err(_e) => return Err(APIError::err("not_logged_in").into()),
+ };
+
+ let user_id = claims.id;
+
+ // Only let admins read this
+ let admins = UserView::admins(&conn)?;
+ let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
+
+ if !admin_ids.contains(&user_id) {
+ return Err(APIError::err("not_an_admin").into());
+ }
+
+ let config_hjson = Settings::read_config_file()?;
+
+ Ok(GetSiteConfigResponse { config_hjson })
+ }
+}
+
+impl Perform<GetSiteConfigResponse> for Oper<SaveSiteConfig> {
+ fn perform(&self, conn: &PgConnection) -> Result<GetSiteConfigResponse, Error> {
+ let data: &SaveSiteConfig = &self.data;
+
+ let claims = match Claims::decode(&data.auth) {
+ Ok(claims) => claims.claims,
+ Err(_e) => return Err(APIError::err("not_logged_in").into()),
+ };
+
+ let user_id = claims.id;
+
+ // Only let admins read this
+ let admins = UserView::admins(&conn)?;
+ let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
+
+ if !admin_ids.contains(&user_id) {
+ return Err(APIError::err("not_an_admin").into());
+ }
+
+ // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
+ let config_hjson = match Settings::save_config_file(&data.config_hjson) {
+ Ok(config_hjson) => config_hjson,
+ Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
+ };
+
+ Ok(GetSiteConfigResponse { config_hjson })
+ }
+}
diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs
index 29a360e4..36a55f96 100644
--- a/server/src/routes/api.rs
+++ b/server/src/routes/api.rs
@@ -52,6 +52,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/api/v1/site", web::post().to(route_post::<CreateSite, SiteResponse>))
.route("/api/v1/site", web::put().to(route_post::<EditSite, SiteResponse>))
.route("/api/v1/site/transfer", web::post().to(route_post::<TransferSite, GetSiteResponse>))
+ .route("/api/v1/site/config", web::get().to(route_get::<GetSiteConfig, GetSiteConfigResponse>))
+ .route("/api/v1/site/config", web::put().to(route_post::<SaveSiteConfig, GetSiteConfigResponse>))
.route("/api/v1/admin/add", web::post().to(route_post::<AddAdmin, AddAdminResponse>))
.route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>))
// User account actions
diff --git a/server/src/routes/index.rs b/server/src/routes/index.rs
index c1c363c9..45ce204e 100644
--- a/server/src/routes/index.rs
+++ b/server/src/routes/index.rs
@@ -33,6 +33,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/modlog/community/{community_id}", web::get().to(index))
.route("/modlog", web::get().to(index))
.route("/setup", web::get().to(index))
+ .route("/admin", web::get().to(index))
.route(
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
web::get().to(index),
diff --git a/server/src/settings.rs b/server/src/settings.rs
index 875323e9..216c057e 100644
--- a/server/src/settings.rs
+++ b/server/src/settings.rs
@@ -1,6 +1,8 @@
use config::{Config, ConfigError, Environment, File};
+use failure::Error;
use serde::Deserialize;
use std::env;
+use std::fs;
use std::net::IpAddr;
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
@@ -112,4 +114,14 @@ impl Settings {
pub fn api_endpoint(&self) -> String {
format!("{}/api/v1", self.hostname)
}
+
+ pub fn read_config_file() -> Result<String, Error> {
+ Ok(fs::read_to_string(CONFIG_FILE)?)
+ }
+
+ pub fn save_config_file(data: &str) -> Result<String, Error> {
+ fs::write(CONFIG_FILE, data)?;
+ Self::init()?;
+ Self::read_config_file()
+ }
}
diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs
index a1feede2..c7136423 100644
--- a/server/src/websocket/mod.rs
+++ b/server/src/websocket/mod.rs
@@ -46,4 +46,6 @@ pub enum UserOperation {
GetPrivateMessages,
UserJoin,
GetComments,
+ GetSiteConfig,
+ SaveSiteConfig,
}
diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs
index 831f12ee..0f2d2d26 100644
--- a/server/src/websocket/server.rs
+++ b/server/src/websocket/server.rs
@@ -708,6 +708,16 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
res.online = chat.sessions.len();
to_json_string(&user_operation, &res)
}
+ UserOperation::GetSiteConfig => {
+ let get_site_config: GetSiteConfig = serde_json::from_str(data)?;
+ let res = Oper::new(get_site_config).perform(&conn)?;
+ to_json_string(&user_operation, &res)
+ }
+ UserOperation::SaveSiteConfig => {
+ let save_site_config: SaveSiteConfig = serde_json::from_str(data)?;
+ let res = Oper::new(save_site_config).perform(&conn)?;
+ to_json_string(&user_operation, &res)
+ }
UserOperation::Search => {
do_user_operation::<Search, SearchResponse>(user_operation, data, &conn)
}
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/