]> Untitled Git - lemmy.git/commitdiff
Adding an admin settings page.
authorDessalines <tyhou13@gmx.com>
Fri, 10 Apr 2020 20:55:57 +0000 (16:55 -0400)
committerDessalines <tyhou13@gmx.com>
Fri, 10 Apr 2020 20:55:57 +0000 (16:55 -0400)
- Fixes #620
- Adding a UserListing component. Fixes #627

25 files changed:
docker/dev/docker-compose.yml
docker/prod/docker-compose.yml
docs/src/contributing_websocket_http_api.md
server/src/api/site.rs
server/src/routes/api.rs
server/src/routes/index.rs
server/src/settings.rs
server/src/websocket/mod.rs
server/src/websocket/server.rs
ui/src/components/admin-settings.tsx [new file with mode: 0644]
ui/src/components/comment-node.tsx
ui/src/components/main.tsx
ui/src/components/navbar.tsx
ui/src/components/post-listing.tsx
ui/src/components/private-message-form.tsx
ui/src/components/private-message.tsx
ui/src/components/search.tsx
ui/src/components/sidebar.tsx
ui/src/components/site-form.tsx
ui/src/components/symbols.tsx
ui/src/components/user-listing.tsx [new file with mode: 0644]
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/services/WebSocketService.ts
ui/translations/en.json

index a7d289b21841a9ebc841ffb92fbadc435ec1bf61..3c52d1e54b28160f34c5e286240e16c7e6b20e4b 100644 (file)
@@ -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
index 325effa65bf2c21e45591283b2781e4729f3e5b3..a1b3616220785b50f2dbaf4e1132bbeddd69ed35 100644 (file)
@@ -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
index a73a1c1339bf5bcb58853c8cc1b3200bda18c941..f228f94e02fcda379bb661eb5f0e2d2457b28b43 100644 (file)
       - [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
index 6bd90149b9df79f551bac6ad7e7bc243bf200125..3720a2c4c1c8a7cf612aa20bd605e5750ab60e12 100644 (file)
@@ -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 })
+  }
+}
index 29a360e4eb42bb010d12c804229e6edccece6371..36a55f960cf48f877016c49fe5644f74e98f152a 100644 (file)
@@ -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
index c1c363c982a76c26cc7599b7cc227afa7ffc3624..45ce204e8a661604fb5249f2c32fbe4920feaa72 100644 (file)
@@ -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),
index 875323e96e125c837cb52110ccd2001f115d4936..216c057e4ee7fcdd180c9a757618750a2cd6e324 100644 (file)
@@ -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()
+  }
 }
index a1feede257e3fae45831f532424a64452329987b..c7136423c08d12f1d5244494642119caf11fff9a 100644 (file)
@@ -46,4 +46,6 @@ pub enum UserOperation {
   GetPrivateMessages,
   UserJoin,
   GetComments,
+  GetSiteConfig,
+  SaveSiteConfig,
 }
index 831f12ee1ec8f86aac7079e874ff27a0e89beae2..0f2d2d26fdb4d89700e1c34efcb9738eed8a6f6d 100644 (file)
@@ -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 (file)
index 0000000..56af711
--- /dev/null
@@ -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);
+    }
+  }
+}
index 39f29b5f84eab110dc4bd7ca37f9915e7b923cdb..ba4301e169a0475d9c69c6a9b71b48d4a7b01216 100644 (file)
@@ -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 ? (
index 38003312768168618cf7c4cf10ecd0a3c2fa9ed0..366d3be8f1e408f962e40fcd0332fcee727d181f 100644 (file)
@@ -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;
index d7f3b5a8a1163ec1663f681e74b5cc59c8cc9881..e0d8aff50ad98789073adc97bad7a170638787d9 100644 (file)
@@ -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() {
index 101d18078997b93ff2d6deedd5fcebc2317a9587..d0efa0437225e298c1b382071ea7521a85d7bc6a 100644 (file)
@@ -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')}
index 7e498bae3ca9048ac9f5855591556fd713f6ca13..6b607654b62b943ae4e3a9e71da7d6a2f9ee4e0d 100644 (file)
@@ -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>
index ef128dd4a813329047d4580a217a1042c59412ff..337b165012000f5598dcdd6ff4d670703aec765f 100644 (file)
@@ -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>
index 3fd2f46773e0913bf295b12683471946b66fdc53..b9662fae3abcb9f62ec25b6ee94511f5306db485 100644 (file)
@@ -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
index 0f4a0e10d8f262cdbd900b43a96b9a4a27bdaa02..d66266f6cab0e0e5a0769d8aa381d0c3ba6cb618 100644 (file)
@@ -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>
index df913043ea5324ef387af2715e223f8126b241f3..f0c80585e5dea23c8c791b1108fff636043ba12a 100644 (file)
@@ -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')}
index dae734a80aa5d496d63f24a1ef49e08ecc5460db..87ba879e1dd68b76138b75366d9fdb565a8014c7 100644 (file)
@@ -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 (file)
index 0000000..1f136e0
--- /dev/null
@@ -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>
+    );
+  }
+}
index c56f6c4eaba08e520293bba781769b1aceca7eaa..8e49df9fbb09c5d88b99834a59a4527a4f699a33 100644 (file)
@@ -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`}
+                  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={`/admin`} component={AdminSettings} />
+                <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>
+            <Footer />
           </div>
-          <Footer />
         </BrowserRouter>
       </Provider>
     );
index 0eeeac06d888f89f58fbf6a5c45f4d3dfdfa5618..b77ccac65b657f120df8ae416f7ac43fb988e268 100644 (file)
@@ -43,6 +43,8 @@ export enum UserOperation {
   GetPrivateMessages,
   UserJoin,
   GetComments,
+  GetSiteConfig,
+  SaveSiteConfig,
 }
 
 export enum CommentSortType {
@@ -102,7 +104,6 @@ export interface UserView {
   avatar?: string;
   email?: string;
   matrix_user_id?: string;
-  fedi_name: string;
   published: string;
   number_of_posts: number;
   post_score: number;
@@ -699,6 +700,19 @@ export interface SiteForm {
   auth?: string;
 }
 
+export interface GetSiteConfig {
+  auth?: string;
+}
+
+export interface GetSiteConfigResponse {
+  config_hjson: string;
+}
+
+export interface SiteConfigForm {
+  config_hjson: string;
+  auth?: string;
+}
+
 export interface GetSiteResponse {
   site: Site;
   admins: Array<UserView>;
@@ -846,7 +860,8 @@ export type MessageType =
   | PasswordChangeForm
   | PrivateMessageForm
   | EditPrivateMessageForm
-  | GetPrivateMessagesForm;
+  | GetPrivateMessagesForm
+  | SiteConfigForm;
 
 type ResponseType =
   | SiteResponse
@@ -868,7 +883,8 @@ type ResponseType =
   | BanUserResponse
   | AddAdminResponse
   | PrivateMessageResponse
-  | PrivateMessagesResponse;
+  | PrivateMessagesResponse
+  | GetSiteConfigResponse;
 
 export interface WebSocketResponse {
   op: UserOperation;
index 02c97cc944badf6db31833066fca3dd845508011..f18b518b92915e57789d870bfe676002f9d4527d 100644 (file)
@@ -40,6 +40,8 @@ import {
   GetPrivateMessagesForm,
   GetCommentsForm,
   UserJoinForm,
+  GetSiteConfig,
+  SiteConfigForm,
   MessageType,
   WebSocketJsonResponse,
 } from '../interfaces';
@@ -268,6 +270,12 @@ export class WebSocketService {
     this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {}));
   }
 
+  public getSiteConfig() {
+    let siteConfig: GetSiteConfig = {};
+    this.setAuth(siteConfig);
+    this.ws.send(this.wsSendWrapper(UserOperation.GetSiteConfig, siteConfig));
+  }
+
   public search(form: SearchForm) {
     this.setAuth(form, false);
     this.ws.send(this.wsSendWrapper(UserOperation.Search, form));
@@ -314,6 +322,11 @@ export class WebSocketService {
     this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));
   }
 
+  public saveSiteConfig(form: SiteConfigForm) {
+    this.setAuth(form);
+    this.ws.send(this.wsSendWrapper(UserOperation.SaveSiteConfig, form));
+  }
+
   private wsSendWrapper(op: UserOperation, data: MessageType) {
     let send = { op: UserOperation[op], data: data };
     console.log(send);
index 0281aaf4bd97ff4d21ae791a511fa0d1360b62ef..a39496abafc29ee4b759e861b5e531f8fc813896 100644 (file)
@@ -53,6 +53,8 @@
     "mods": "mods",
     "moderates": "Moderates",
     "settings": "Settings",
+    "admin_settings": "Admin Settings",
+    "site_config": "Site Configuration",
     "remove_as_mod": "remove as mod",
     "appoint_as_mod": "appoint as mod",
     "modlog": "Modlog",
@@ -78,6 +80,7 @@
     "unban": "unban",
     "unban_from_site": "unban from site",
     "banned": "banned",
+    "banned_users": "Banned Users",
     "save": "save",
     "unsave": "unsave",
     "create": "create",
       "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
     "not_logged_in": "Not logged in.",
     "logged_in": "Logged in.",
+    "site_saved": "Site Saved.",
     "community_ban": "You have been banned from this community.",
     "site_ban": "You have been banned from the site",
     "couldnt_create_comment": "Couldn't create comment.",