diff --git a/internal/web/profile.go b/internal/web/profile.go
index c6bc5ee5a..a4332b0c1 100644
--- a/internal/web/profile.go
+++ b/internal/web/profile.go
@@ -117,6 +117,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
"show_back_to_top": showBackToTop,
"stylesheets": stylesheets,
"javascript": []string{
+ "/assets/dist/bundle.js",
"/assets/dist/frontend.js",
},
})
diff --git a/internal/web/panels.go b/internal/web/settings-panel.go
similarity index 60%
rename from internal/web/panels.go
rename to internal/web/settings-panel.go
index fdec87a33..3ba396998 100644
--- a/internal/web/panels.go
+++ b/internal/web/settings-panel.go
@@ -27,7 +27,7 @@
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
-func (m *Module) UserPanelHandler(c *gin.Context) {
+func (m *Module) SettingsPanelHandler(c *gin.Context) {
host := config.GetHost()
instance, err := m.processor.InstanceGet(c.Request.Context(), host)
if err != nil {
@@ -41,37 +41,13 @@ func (m *Module) UserPanelHandler(c *gin.Context) {
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
assetsPathPrefix + "/dist/_colors.css",
assetsPathPrefix + "/dist/base.css",
- assetsPathPrefix + "/dist/panels-base.css",
- assetsPathPrefix + "/dist/panels-user-style.css",
+ assetsPathPrefix + "/dist/profile.css",
+ assetsPathPrefix + "/dist/status.css",
+ assetsPathPrefix + "/dist/settings-panel-style.css",
},
"javascript": []string{
assetsPathPrefix + "/dist/bundle.js",
- assetsPathPrefix + "/dist/user-panel.js",
- },
- })
-}
-
-// TODO: abstract the {admin, user}panel handlers in some way
-func (m *Module) AdminPanelHandler(c *gin.Context) {
- host := config.GetHost()
- instance, err := m.processor.InstanceGet(c.Request.Context(), host)
- if err != nil {
- api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
- return
- }
-
- c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
- "instance": instance,
- "stylesheets": []string{
- assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
- assetsPathPrefix + "/dist/_colors.css",
- assetsPathPrefix + "/dist/base.css",
- assetsPathPrefix + "/dist/panels-base.css",
- assetsPathPrefix + "/dist/panels-admin-style.css",
- },
- "javascript": []string{
- assetsPathPrefix + "/dist/bundle.js",
- assetsPathPrefix + "/dist/admin-panel.js",
+ assetsPathPrefix + "/dist/settings.js",
},
})
}
diff --git a/internal/web/thread.go b/internal/web/thread.go
index 48c53c448..fb5006c34 100644
--- a/internal/web/thread.go
+++ b/internal/web/thread.go
@@ -119,6 +119,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
"ogMeta": ogBase(instance).withStatus(status),
"stylesheets": stylesheets,
"javascript": []string{
+ "/assets/dist/bundle.js",
"/assets/dist/frontend.js",
},
})
diff --git a/internal/web/web.go b/internal/web/web.go
index a74fc8e19..a816f3f08 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -37,9 +37,9 @@
profilePath = "/@:" + usernameKey
customCSSPath = profilePath + "/custom.css"
statusPath = profilePath + "/statuses/:" + statusIDKey
- adminPanelPath = "/admin"
- userPanelpath = "/user"
assetsPathPrefix = "/assets"
+ userPanelPath = "/settings/user"
+ adminPanelPath = "/settings/admin"
tokenParam = "token"
usernameKey = "username"
@@ -70,20 +70,24 @@ func (m *Module) Route(s router.Router) error {
assetsGroup := s.AttachGroup(assetsPathPrefix)
m.mountAssetsFilesystem(assetsGroup)
- s.AttachHandler(http.MethodGet, adminPanelPath, m.AdminPanelHandler)
- // redirect /admin/ to /admin
- s.AttachHandler(http.MethodGet, adminPanelPath+"/", func(c *gin.Context) {
- c.Redirect(http.StatusMovedPermanently, adminPanelPath)
+ s.AttachHandler(http.MethodGet, "/settings", m.SettingsPanelHandler)
+ s.AttachHandler(http.MethodGet, "/settings/*panel", m.SettingsPanelHandler)
+
+ // User panel redirects
+ // used by clients
+ s.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) {
+ c.Redirect(http.StatusMovedPermanently, userPanelPath)
})
- s.AttachHandler(http.MethodGet, userPanelpath, m.UserPanelHandler)
- // redirect /user/ to /user
- s.AttachHandler(http.MethodGet, userPanelpath+"/", func(c *gin.Context) {
- c.Redirect(http.StatusMovedPermanently, userPanelpath)
+ // old version of settings panel
+ s.AttachHandler(http.MethodGet, "/user", func(c *gin.Context) {
+ c.Redirect(http.StatusMovedPermanently, userPanelPath)
})
- // redirect /auth/edit to /user
- s.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) {
- c.Redirect(http.StatusMovedPermanently, userPanelpath)
+
+ // Admin panel redirects
+ // old version of settings panel
+ s.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) {
+ c.Redirect(http.StatusMovedPermanently, adminPanelPath)
})
// serve front-page
diff --git a/web/source/css/_colors.css b/web/source/css/_colors.css
index 1544e6ad0..fb07758aa 100644
--- a/web/source/css/_colors.css
+++ b/web/source/css/_colors.css
@@ -23,57 +23,85 @@
/* Color definitions */
-$near_white: #fafaff;
+/* Foreground */
+$white1: #fafaff; /* default text color, contrast >= 5.0 with all $grays */
+$white2: #b3b5c6; /* less important text, can be used with $gray1 (6.8), $gray2 (5.5), $gray3 (4.9), $gray4 (4.5) */
-$sloth_gray1: #b0b0b5;
-$sloth_gray2: #4d4e56;
+/* Background shades, contrast >= 5.0 with $white1 (#fafaff) */
+$gray1: #2a2b2f;
+$gray2: #35363b;
+$gray3: #3a3b41;
+$gray4: #45464e;
+$gray5: #4d4e56;
+$gray6: #575861;
+$gray7: #5d5e67;
+$gray8: #696a75;
-$sloth_orange1: #e78e5a;
-$sloth_orange2: #D87841;
-$blue: #63b1de; // complementary color to $sloth_orange1
+$orange1: #fd6a00; /* Used for non-text accent colors, can be used as background: $gray1 for text color (contrast 4.6)*/
+$orange2: #ff853e; /* hover/selected accent to $orange1, can be used with $gray1 (5.7), $gray2 (4.6) */
-/* derivative colors */
+$blue1: #3a9fde; /* darker blue for smaller elements (borders), can only be used with $gray1 (4.7) */
+$blue2: #66befe; /* all-round accent color, can be used with $gray1 (6.8), $gray2 (5.5), $gray3 (4.9), $gray4 (4.5) */
+$blue3: #89caff; /* hover/selected accent to $blue2, can be used with $gray1 (7.9), $gray2 (6.3), $gray3 (5.6), $gray4 (5.2), $gray5 (4.7) */
-$sloth_gray2_darker3: color-mod($sloth_gray2 lightness(-3%));
-$sloth_gray2_darker5: color-mod($sloth_gray2 lightness(-5%));
-$sloth_gray2_darker7: color-mod($sloth_gray2 lightness(-7%));
-$sloth_gray2_darker15: color-mod($sloth_gray2 lightness(-15%));
-$sloth_gray2_lighter3: color-mod($sloth_gray2 lightness(+3%));
-$sloth_gray2_lighter5: color-mod($sloth_gray2 lightness(+5%));
+$error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */
+$error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */
+$error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */
-$blue_lighter8: color-mod($blue lightness(+4%));
-$lightblue: color-mod($blue lightness(+16%));
+$fg: $white1;
+$bg: $gray1;
-$fg: $near_white;
-$bg: $sloth_gray2_darker7;
+$bg-trans: color-mod($gray5 alpha(62%));
-$bg_trans: color-mod($sloth_gray2 alpha(62%));
-
-$bg_accent: $sloth_gray2_lighter3;
-$fg_accent: $lightblue;
-$border_accent: $sloth_orange2;
+$bg-accent: $gray5;
+$fg-accent: $blue3;
+$fg-reduced: $white2;
+$border-accent: $orange2;
/* Color variables as used in a specific location */
-$footer_bg: $bg_accent;
+$link-fg: $fg-accent;
-$link_fg: $fg_accent;
+$button-bg: $blue2;
+$button-fg: $gray1;
+$button-hover-bg: $blue3;
-$button_border: 0.08rem solid color-mod($sloth_orange2 lightness(-15%));
-$button_bg: $blue_lighter8;
-$button_fg: $sloth_gray2_darker15;
-$button_hover_bg: $lightblue;
+$button-danger-bg: $orange1;
+$button-danger-fg: $gray1;
+$button-danger-hover-bg: $orange2;
-$status_focus_bg: $bg_accent;
-$status_unfocus_bg: $sloth_gray2_darker3;
-$status_info_fg: #CBCBD7;
+$toot-focus-bg: $gray5;
+$toot-unfocus-bg: $gray3;
-$bg_no_img_desc: $sloth_orange2;
-$bg_sensitive: $sloth_gray2_darker15;
+$toot-info-bg: $gray4;
+
+$no-img-desc-bg: $orange1;
+$no-img-desc-fg: $gray1;
+
+$bg-sensitive: $gray1;
$boxshadow: 0 0.4rem 1rem -0.1rem rgba(0,0,0,0.15);
-$boxshadow_border: 0.08rem solid $sloth_gray2_darker5;
+$boxshadow-border: 0.08rem solid $gray1;
-$profile_avatar_border: 0.2rem solid $border_accent;
+$avatar-border: $orange2;
-$input_bg: $sloth_gray2_darker3;
\ No newline at end of file
+$input-bg: $gray4;
+$input-disabled-bg: $gray2;
+$input-border: $blue1;
+$input-focus-border: $blue3;
+
+$settings-nav-bg: $bg-accent;
+$settings-nav-header-fg: $gray1;
+$settings-nav-header-bg: $orange1;
+
+$settings-nav-bg-hover: $gray3;
+/* $settings-nav-fg-hover: $gray1; */
+
+$settings-nav-bg-active: $gray2;
+/* $settings-nav-fg-active: $orange2; */
+
+$error-fg: $error1;
+$error-bg: $error2;
+
+$settings-entry-bg: $gray3;
+$settings-entry-hover-bg: $gray4;
\ No newline at end of file
diff --git a/web/source/css/base.css b/web/source/css/base.css
index 3cdf19fe8..d8a79685e 100644
--- a/web/source/css/base.css
+++ b/web/source/css/base.css
@@ -34,7 +34,7 @@
$br: 0.4rem;
// border radius for items that are framed/bordered
// inside something with $br, eg avatar, header img
-$br_inner: 0.2rem;
+$br-inner: 0.2rem;
html, body {
padding: 0;
@@ -42,7 +42,7 @@ html, body {
background: $bg;
color: $fg;
font-family: "Noto Sans", sans-serif;
- scrollbar-color: $sloth_orange1 $sloth_gray2_darker3;
+ scrollbar-color: $orange1 $gray3;
}
body {
@@ -71,7 +71,7 @@ h1 {
}
a {
- color: $link_fg;
+ color: $link-fg;
}
header, footer {
@@ -83,9 +83,13 @@ header, footer {
align-self: start;
}
+header {
+ display: flex;
+ justify-content: center;
+}
+
header a {
margin: 2rem;
- /* background: $header_bg; */
display: flex;
flex-direction: column;
flex-wrap: wrap;
@@ -109,7 +113,7 @@ header a {
}
}
-.excerpt_top {
+.excerpt-top {
margin-top: -1rem;
margin-bottom: 2rem;
font-style: italic;
@@ -119,15 +123,15 @@ header a {
.count {
font-weight: bold;
- color: $fg_accent;
+ color: $fg-accent;
}
}
main {
section {
- background: $bg_accent;
+ background: $bg-accent;
box-shadow: $boxshadow;
- border: $boxshadow_border;
+ border: $boxshadow-border;
border-radius: $br;
padding: 2rem;
margin-bottom: 2rem;
@@ -144,10 +148,10 @@ main {
.button, button {
border-radius: 0.2rem;
- color: $button_fg;
- background: $button_bg;
+ color: $button-fg;
+ background: $button-bg;
box-shadow: $boxshadow;
- border: $button_border;
+ border: $button-border;
text-decoration: none;
font-size: 1.2rem;
font-weight: bold;
@@ -157,8 +161,17 @@ main {
text-align: center;
font-family: 'Noto Sans', sans-serif;
+ &.danger {
+ color: $button-danger-fg;
+ background: $button-danger-bg;
+
+ &:hover {
+ background: $button-danger-hover-bg;
+ }
+ }
+
&:hover {
- background: $button_hover_bg;
+ background: $button-hover-bg;
}
}
@@ -191,7 +204,7 @@ section.apps {
grid-template-columns: 25% 1fr;
gap: 1.5rem;
padding: 0.5rem;
- background: $bg_accent;
+ background: $bg-accent;
border-radius: 0.5rem;
.logo {
@@ -211,7 +224,7 @@ section.apps {
}
div {
- padding: 1rem 0;
+ padding: 0;
h3 {
margin-top: 0;
}
@@ -264,26 +277,42 @@ section.error {
}
}
+.error-text {
+ color: $error1;
+ background: $error2;
+ border-radius: 0.1rem;
+ font-weight: bold;
+}
+
input, select, textarea {
box-sizing: border-box;
- border: 0.15rem solid $border_accent;
+ border: 0.15rem solid $input-border;
border-radius: 0.1rem;
color: $fg;
- /* background: $input_bg; */
- background: $bg_accent;
+ background: $input-bg;
width: 100%;
font-family: 'Noto Sans', sans-serif;
font-size: 1rem;
padding: 0.3rem;
&:focus {
- border-color: $fg_accent;
+ border-color: $input-focus-border;
+ }
+
+ &:disabled {
+ background: $input-disabled-bg;
}
}
-input, textarea {
- padding-top: 0.1rem;
- padding-bottom: 0.1rem;
+::placeholder {
+ opacity: 1;
+ color: $fg-reduced
+}
+
+hr {
+ color: transparent;
+ width: 100%;
+ border-bottom: 0.02rem solid $border-accent;
}
footer {
@@ -330,4 +359,8 @@ footer {
margin: -0.5ex 0 0;
object-fit: contain;
vertical-align: middle;
+}
+
+.monospace {
+ font-family: monospace;
}
\ No newline at end of file
diff --git a/web/source/css/profile.css b/web/source/css/profile.css
index 4fa0b6247..01ec077ac 100644
--- a/web/source/css/profile.css
+++ b/web/source/css/profile.css
@@ -28,7 +28,7 @@ main {
}
.profile {
- background: $bg_accent;
+ background: $bg-accent;
display: grid;
grid-template-rows: auto auto auto;
grid-template-columns: auto;
@@ -38,7 +38,7 @@ main {
border-radius: $br;
box-shadow: $boxshadow;
- border: $boxshadow_border;
+ border: $boxshadow-border;
.headerimage {
width: 100%;
@@ -50,7 +50,7 @@ main {
width: 100%;
height: 100%;
object-fit: cover;
- border-radius: $br_inner $br_inner 0 0;
+ border-radius: $br-inner $br-inner 0 0;
}
}
@@ -69,7 +69,7 @@ main {
#profile-basic-filler2 {
grid-area: filler2;
- background: $bg_trans;
+ background: $bg-trans;
}
.avatar {
@@ -79,7 +79,7 @@ main {
width: 8.5rem;
grid-area: avatar;
background: $bg;
- border: $profile_avatar_border;
+ border: 0.2rem solid $avatar-border;
padding: 0;
border-radius: $br;
position: relative;
@@ -87,7 +87,7 @@ main {
box-shadow: $boxshadow;
img {
object-fit: cover;
- border-radius: $br_inner;
+ border-radius: $br-inner;
width: 100%;
height: 100%;
}
@@ -105,7 +105,7 @@ main {
font-weight: bold;
font-size: 2rem;
line-height: 2.2rem;
- background: $bg_trans;
+ background: $bg-trans;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
@@ -120,7 +120,7 @@ main {
padding-top: 0;
margin-top: 0.25rem;
padding-bottom: 0.25rem;
- color: $fg_accent;
+ color: $fg-accent;
font-weight: bold;
word-break: break-all;
text-overflow: ellipsis;
diff --git a/web/source/css/status.css b/web/source/css/status.css
index 9a83a7fd5..e34b3b091 100644
--- a/web/source/css/status.css
+++ b/web/source/css/status.css
@@ -31,13 +31,13 @@ main {
}
.toot {
- background: $status_unfocus_bg;
+ background: $toot-unfocus-bg;
box-shadow: $boxshadow;
- border: $boxshadow_border;
+ border: $boxshadow-border;
position: relative;
margin-bottom: $br;
- border-radius: $br;
- padding: 1.5rem 0;
+ padding-top: 1.5rem;
+ padding-bottom: 0.7rem;
a {
position: relative;
@@ -49,27 +49,34 @@ main {
.contentgrid {
padding: 0 1.5rem;
display: grid;
- grid-template-columns: 4rem auto 1fr;
- grid-template-rows: 1.5rem auto auto;
+ grid-template-columns: 4rem 1fr auto;
+ grid-template-rows: 1.5rem auto auto auto;
column-gap: 0.5rem;
}
+ .not-expanded {
+ color: $fg-reduced;
+ grid-column: 3;
+ grid-row: 1;
+ }
+
.avatar {
- grid-row: span 2;
+ grid-row: span 3;
aspect-ratio: 1/1;
+ display: flex;
+ border: 0.2rem solid $avatar-border;
+ border-radius: 0.4rem;
+ overflow: hidden; /* hides corners from img overflowing */
img {
height: 100%;
width: 100%;
object-fit: cover;
background: $bg;
- border: 0.1rem solid $acc2;
- border-radius: calc($br / 1.5);
}
}
.displayname {
- grid-column: span 2;
font-weight: bold;
font-size: 1.2rem;
line-height: 2rem;
@@ -82,7 +89,7 @@ main {
}
.username {
- color: $link_fg;
+ color: $link-fg;
line-height: 2rem;
margin-top: -0.5rem;
align-self: start;
@@ -119,8 +126,7 @@ main {
.text {
margin: 0;
- margin-top: 0.5rem;
- grid-column: span 3;
+ grid-column: 2 / span 2;
grid-row: span 1;
overflow: hidden;
@@ -128,34 +134,33 @@ main {
z-index: 2;
a {
- color: $link_fg;
+ color: $link-fg;
text-decoration: underline;
}
.content {
- padding-top: 0.5rem;
padding-bottom: 0.5rem;
word-break: break-word;
blockquote {
padding: 0.5rem 0 0.5rem 1.5rem;
- border-left: 0.2rem solid $sloth_orange1;
+ border-left: 0.2rem solid $border-accent;
margin-left: 1rem;
font-style: italic;
}
hr {
- border: 1px dashed $sloth_orange1;
+ border: 1px dashed $border-accent;
}
pre, code {
- background-color: $sloth_gray2_darker7;
+ background-color: $gray2;
}
code {
padding: 0.25rem;
- border-radius: $br_inner;
+ border-radius: $br-inner;
}
pre {
@@ -249,7 +254,7 @@ main {
.closed {
transition: 0.3s;
- background: $bg_sensitive;
+ background: $bg-sensitive;
@supports (backdrop-filter: blur(2rem)) {
background: transparent;
backdrop-filter: blur(2rem);
@@ -263,17 +268,17 @@ main {
}
.no-image-desc {
- color: $button_fg;
+ color: $no-img-desc-fg;
+ background: $no-img-desc-bg;
display: flex;
position: absolute;
bottom: 0.1rem;
right: 0.4rem;
margin-bottom: 0.4rem;
margin-right: 0.4rem;
- background: $bg_no_img_desc;
padding: 0.1rem 0.45rem;
border-radius: 100%;
- border: 0.2rem solid $button_fg;
+ border: 0.2rem solid $button-fg;
z-index: 3;
i.fa {
@@ -302,12 +307,13 @@ main {
}
.info {
+ background: $toot-info-bg;
+ color: $fg-reduced;
display: none;
- border-top: 0.15rem solid $status_unfocus_bg;
+ border-top: 0.15rem solid $toot-info-border;
padding: 0.5rem 1.5rem;
div {
- position: relative;
padding-right: 1.3rem;
}
@@ -317,30 +323,6 @@ main {
grid-column: span 3;
flex-wrap: wrap;
-
- div.stats::after {
- display: none;
- }
-
- div::after {
- $size: 0.25rem;
- display: block;
- background: $fg_dark;
- height: $size;
- width: $size;
- content: "";
- position: absolute;
- top: calc((1.5rem - $size) / 2);
- right: 0.55rem;
- border-radius: 1rem;
- }
-
- div:last-child {
- &::after {
- display: none;
- }
- margin-right: 0;
- }
}
.toot-link {
@@ -362,7 +344,7 @@ main {
border-top-right-radius: $br;
}
- &:last-child {
+ &:last-child, &:last-child .info {
/* bottom left, bottom right */
border-bottom-left-radius: $br;
border-bottom-right-radius: $br;
@@ -370,11 +352,21 @@ main {
}
&.expanded {
- background: $status_focus_bg;
+ background: $toot-focus-bg;
padding-bottom: 0;
.contentgrid {
- padding-bottom: 1rem;
+ .displayname {
+ grid-column: span 2;
+ }
+
+ .text {
+ grid-column: 1 / span 3;
+ }
+
+ .not-expanded {
+ display: none;
+ }
}
.info {
diff --git a/web/source/dev-server.js b/web/source/dev-server.js
index 3802eb88c..46baad24a 100644
--- a/web/source/dev-server.js
+++ b/web/source/dev-server.js
@@ -1,19 +1,19 @@
/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
*/
"use strict";
diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js
index 5c53a31bf..2a54d52b6 100644
--- a/web/source/frontend/index.js
+++ b/web/source/frontend/index.js
@@ -18,11 +18,6 @@
"use strict";
-
-// WARNING: currently dependencies get deduplicated with factor-bundle, but
-// our frontend templates don't load the common bundle.js since it contains React etc
-// so we can't use any dependencies that would deduplicate with the other files
-
const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js");
const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js");
const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default;
diff --git a/web/source/index.js b/web/source/index.js
index 20e8ee623..218e420ed 100644
--- a/web/source/index.js
+++ b/web/source/index.js
@@ -1,19 +1,19 @@
/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
*/
"use strict";
@@ -23,7 +23,8 @@
*/
const path = require('path');
-const budoExpress = require('budo-express');
+// Forked budo-express supports EventEmitter, to write bundle.js to disk in development
+const budoExpress = require('@f0x52/budo-express');
const babelify = require('babelify');
const fs = require("fs");
const EventEmitter = require('events');
@@ -38,8 +39,9 @@ const splitCSS = require("./lib/split-css.js");
const bundles = {
"./frontend/index.js": "frontend.js",
- "./panels/admin/index.js": "admin-panel.js",
- "./panels/user/index.js": "user-panel.js",
+ "./settings-panel/index.js": "settings.js",
+ // "./panels/admin/index.js": "admin-panel.js",
+ // "./panels/user/index.js": "user-panel.js",
};
const postcssPlugins = [
@@ -50,6 +52,18 @@ const postcssPlugins = [
"postcss-color-mod-function"
].map((plugin) => require(plugin)());
+let uglifyifyInProduction;
+
+if (process.env.NODE_ENV != "development") {
+ console.log("uglifyify'ing production bundles");
+ uglifyifyInProduction = [
+ require("uglifyify"), {
+ global: true,
+ exts: ".js"
+ }
+ ];
+}
+
const browserifyConfig = {
transform: [
[
@@ -69,10 +83,7 @@ const browserifyConfig = {
exclude: /node_modules\/(?!photoswipe-dynamic-caption-plugin)/,
}
],
- [require("uglifyify"), {
- global: true,
- exts: ".js"
- }]
+ uglifyifyInProduction
],
plugin: [
[require("icssify"), {
@@ -86,7 +97,8 @@ const browserifyConfig = {
return out(file);
})
}]
- ]
+ ],
+ extensions: [".js", ".jsx", ".css"]
};
const entryFiles = Object.keys(bundles);
diff --git a/web/source/lib/split-css.js b/web/source/lib/split-css.js
index fb8694095..da5602e1c 100644
--- a/web/source/lib/split-css.js
+++ b/web/source/lib/split-css.js
@@ -1,19 +1,19 @@
/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
*/
"use strict";
diff --git a/web/source/lib/submit.js b/web/source/lib/submit.js
deleted file mode 100644
index ae4108a01..000000000
--- a/web/source/lib/submit.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const React = require("react");
-
-module.exports = function Submit({onClick, label, errorMsg, statusMsg}) {
- return (
-
-
-
{errorMsg ? errorMsg : statusMsg}
-
- );
-};
diff --git a/web/source/package.json b/web/source/package.json
index 691b68183..6e8deba09 100644
--- a/web/source/package.json
+++ b/web/source/package.json
@@ -1,6 +1,6 @@
{
"name": "gotosocial-frontend",
- "version": "0.3.8",
+ "version": "0.5.0",
"description": "GoToSocial frontend sources",
"main": "index.js",
"author": "f0x",
@@ -9,18 +9,23 @@
"@babel/core": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/preset-react": "^7.12.13",
+ "@f0x52/budo-express": "^1.1.0",
+ "@reduxjs/toolkit": "^1.8.5",
"autoprefixer": "^10.4.8",
"babelify": "^10.0.0",
"bluebird": "^3.7.2",
"browserify": "^17.0.0",
"browserlist": "^1.0.1",
- "budo-express": "^1.0.8",
+ "create-error": "^0.3.1",
"css-extract": "^2.0.0",
+ "default-value": "^1.0.0",
+ "dotty": "^0.1.2",
"eslint-plugin-react": "^7.24.0",
"express": "^4.18.1",
"factor-bundle": "^2.5.0",
- "from2-string": "^1.1.0",
"icssify": "^2.0.0",
+ "is-plain-object": "^5.0.0",
+ "is-valid-domain": "^0.1.6",
"js-file-download": "^0.4.12",
"modern-normalize": "^1.1.0",
"photoswipe": "^5.3.0",
@@ -31,11 +36,17 @@
"postcss-nested": "^5.0.6",
"postcss-scss": "^4.0.4",
"postcss-strip-inline-comments": "^0.1.5",
- "pretty-bytes": "^5.6.0",
- "react": "^17.0.1",
- "react-dom": "^17.0.1",
- "reactify": "^1.1.1",
- "uglifyify": "^5.0.2"
+ "prettier-bytes": "^1.0.4",
+ "pretty-bytes": "4",
+ "react": "18",
+ "react-dom": "18",
+ "react-error-boundary": "^3.1.4",
+ "react-redux": "^8.0.2",
+ "redux-devtools-extension": "^2.13.9",
+ "redux-persist": "^6.0.0",
+ "redux-thunk": "^2.4.1",
+ "uglifyify": "^5.0.2",
+ "wouter": "^2.8.0-alpha.2"
},
"devDependencies": {
"@f0x52/eslint-config-react": "^1.1.0",
diff --git a/web/source/panels/admin/README.md b/web/source/panels/admin/README.md
deleted file mode 100644
index 9a4572270..000000000
--- a/web/source/panels/admin/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# GoToSocial Admin Panel
-
-Standalone web admin panel for [GoToSocial](https://github.com/superseriousbusiness/gotosocial).
-
-A public hosted instance is also available at https://gts.superseriousbusiness.org/admin/, so you can fill your own instance URL in there.
-
-## Installation
-Build requirements: some version of Node.js with npm,
-```
-git clone https://github.com/superseriousbusiness/gotosocial-admin.git && cd gotosocial-admin
-npm install
-node index.js
-```
-All processed build output will now be in `public/`, which you can copy over to a folder in your GoToSocial installation like `web/assets/admin`, or serve elsewhere.
-No further configuration is required, authentication happens through normal OAUTH flow.
-
-## Development
-Follow the installation steps, but run `NODE_ENV=development node index.js` to start the livereloading dev server instead.
-
-## License, donations
-[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html). If you want to support my work, you can:
\ No newline at end of file
diff --git a/web/source/panels/admin/blocks.js b/web/source/panels/admin/blocks.js
deleted file mode 100644
index b12eb50a9..000000000
--- a/web/source/panels/admin/blocks.js
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-const React = require("react");
-const fileDownload = require("js-file-download");
-
-function sortBlocks(blocks) {
- return blocks.sort((a, b) => { // alphabetical sort
- return a.domain.localeCompare(b.domain);
- });
-}
-
-function deduplicateBlocks(blocks) {
- let a = new Map();
- blocks.forEach((block) => {
- a.set(block.id, block);
- });
- return Array.from(a.values());
-}
-
-module.exports = function Blocks({oauth}) {
- const [blocks, setBlocks] = React.useState([]);
- const [info, setInfo] = React.useState("Fetching blocks");
- const [errorMsg, setError] = React.useState("");
- const [checked, setChecked] = React.useState(new Set());
-
- React.useEffect(() => {
- Promise.try(() => {
- return oauth.apiRequest("/api/v1/admin/domain_blocks", undefined, undefined, "GET");
- }).then((json) => {
- setInfo("");
- setError("");
- setBlocks(sortBlocks(json));
- }).catch((e) => {
- setError(e.message);
- setInfo("");
- });
- }, []);
-
- let blockList = blocks.map((block) => {
- function update(e) {
- let newChecked = new Set(checked.values());
- if (e.target.checked) {
- newChecked.add(block.id);
- } else {
- newChecked.delete(block.id);
- }
- setChecked(newChecked);
- }
-
- return (
-
-
-
-//
-// );
-// }
\ No newline at end of file
diff --git a/web/source/panels/admin/index.js b/web/source/panels/admin/index.js
deleted file mode 100644
index 0fc1601eb..000000000
--- a/web/source/panels/admin/index.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-const React = require("react");
-const ReactDom = require("react-dom");
-
-const createPanel = require("../lib/panel");
-
-const Settings = require("./settings");
-const Blocks = require("./blocks");
-
-require("../base.css");
-require("./style.css");
-
-function AdminPanel({oauth}) {
- /*
- Features: (issue #78)
- - [ ] Instance information updating
- GET /api/v1/instance PATCH /api/v1/instance
- - [ ] Domain block creation, viewing, and deletion
- GET /api/v1/admin/domain_blocks
- POST /api/v1/admin/domain_blocks
- GET /api/v1/admin/domain_blocks/DOMAIN_BLOCK_ID, DELETE /api/v1/admin/domain_blocks/DOMAIN_BLOCK_ID
- - [ ] Blocklist import/export
- GET /api/v1/admin/domain_blocks?export=true
- POST json file as form field domains to /api/v1/admin/domain_blocks
- */
-
- return (
-
-
-
-
-
- );
-}
-
-function Logout({oauth}) {
- return (
-
-
-
- );
-}
-
-createPanel("GoToSocial Admin Panel", ["admin"], AdminPanel);
\ No newline at end of file
diff --git a/web/source/panels/admin/settings.js b/web/source/panels/admin/settings.js
deleted file mode 100644
index c9f470464..000000000
--- a/web/source/panels/admin/settings.js
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-const React = require("react");
-
-module.exports = function Settings({oauth}) {
- const [info, setInfo] = React.useState({});
- const [errorMsg, setError] = React.useState("");
- const [statusMsg, setStatus] = React.useState("Fetching instance info");
-
- React.useEffect(() => {
- Promise.try(() => {
- return oauth.apiRequest("/api/v1/instance", "GET");
- }).then((json) => {
- setInfo(json);
- }).catch((e) => {
- setError(e.message);
- setStatus("");
- });
- }, []);
-
- function submit() {
- setStatus("PATCHing");
- setError("");
- return Promise.try(() => {
- let formDataInfo = new FormData();
- Object.entries(info).forEach(([key, val]) => {
- if (key == "contact_account") {
- key = "contact_username";
- val = val.username;
- }
- if (key == "email") {
- key = "contact_email";
- }
- if (typeof val != "object") {
- formDataInfo.append(key, val);
- }
- });
- return oauth.apiRequest("/api/v1/instance", "PATCH", formDataInfo, "form");
- }).then((json) => {
- setStatus("Config saved");
- console.log(json);
- }).catch((e) => {
- setError(e.message);
- setStatus("");
- });
- }
-
- return (
-
-
-
- );
- });
- return (
-
- {path != "" &&
- <>{path}: >
- }
- {listing}
-
- );
-}
\ No newline at end of file
diff --git a/web/source/panels/admin/style.css b/web/source/panels/admin/style.css
deleted file mode 100644
index 01195437f..000000000
--- a/web/source/panels/admin/style.css
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-section.info {
- form {
- grid-template-columns: auto 1fr;
- width: calc(100% - 0.35rem);
-
- input {
- width: 100%;
- line-height: 1.5rem;
- }
-
- label, input {
- padding: 0.2rem 0.5rem;
- }
-
- input[type=checkbox] {
- justify-self: start;
- width: initial;
- }
-
- input:read-only {
- border: none;
- }
-
- input:invalid {
- border-color: red;
- }
- }
-
- textarea {
- width: 100%;
- height: 8rem;
- }
-
- h1 {
- display: flex;
- justify-content: space-between;
- margin-bottom: 0.5rem;
- }
-}
-
-section.blocks {
- .overflow {
- max-height: 80vh;
- overflow-y: auto;
- }
-
- .blocklist {
- display: grid;
- grid-template-columns: auto 1fr auto;
- grid-gap: 0.35rem 0;
-
- div {
- background: rgb(70, 79, 88);
- padding: 0.2rem 0.4rem;
- }
- }
-
- .addblock {
- display: grid;
- grid-template-columns: 1fr auto auto;
- grid-gap: 0.35rem;
-
- input, select {
- font-size: 1.2rem;
- }
-
- input, select, textarea {
- padding: 0.5rem;
- }
-
- div {
- grid-column: 1/4;
- }
-
- div.single input {
- width: initial;
- }
- }
-
- h3 {
- margin-bottom: 0;
- }
-
- .controls {
- display: flex;
- gap: 0.5rem;
- }
-}
diff --git a/web/source/panels/base.css b/web/source/panels/base.css
deleted file mode 100644
index 2d76ed080..000000000
--- a/web/source/panels/base.css
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-body {
- grid-template-rows: auto 1fr;
-}
-
-.capitalize {
- text-transform: capitalize;
-}
-
-section {
- margin-bottom: 1rem;
-}
-
-input, select, textarea {
- box-sizing: border-box;
-}
-
-.error {
- font-weight: bold;
-}
-
-.hidden {
- display: none;
-}
-
-.messagebutton {
- margin-top: 1rem;
- display: flex;
- gap: 1rem;
- align-items: center;
-
- button {
- white-space: nowrap;
- }
-}
-
-.notImplemented {
- border: 2px solid rgb(70, 79, 88);
- background: repeating-linear-gradient(
- -45deg,
- #525c66,
- #525c66 10px,
- rgb(70, 79, 88) 10px,
- rgb(70, 79, 88) 20px
- ) !important;
-}
-
-.mono {
- font-family: monospace;
-}
diff --git a/web/source/panels/lib/oauth.js b/web/source/panels/lib/oauth.js
deleted file mode 100644
index 3619dfa01..000000000
--- a/web/source/panels/lib/oauth.js
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-
-function getCurrentUrl() {
- return window.location.origin + window.location.pathname; // strips ?query=string and #hash
-}
-
-module.exports = function oauthClient(config, initState) {
- /* config:
- instance: instance domain (https://testingtesting123.xyz)
- client_name: "GoToSocial Admin Panel"
- scope: []
- website:
- */
-
- let state = initState;
- if (initState == undefined) {
- state = localStorage.getItem("oauth");
- if (state == undefined) {
- state = {
- config
- };
- storeState();
- } else {
- state = JSON.parse(state);
- }
- }
-
- function storeState() {
- localStorage.setItem("oauth", JSON.stringify(state));
- }
-
- /* register app
- /api/v1/apps
- */
- function register() {
- if (state.client_id != undefined) {
- return true; // we already have a registration
- }
- let url = new URL(config.instance);
- url.pathname = "/api/v1/apps";
-
- return fetch(url.href, {
- method: "POST",
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- client_name: config.client_name,
- redirect_uris: getCurrentUrl(),
- scopes: config.scope.join(" "),
- website: getCurrentUrl()
- })
- }).then((res) => {
- if (res.status != 200) {
- throw res;
- }
- return res.json();
- }).then((json) => {
- state.client_id = json.client_id;
- state.client_secret = json.client_secret;
- storeState();
- });
- }
-
- /* authorize:
- /oauth/authorize
- ?client_id=CLIENT_ID
- &redirect_uri=window.location.href
- &response_type=code
- &scope=admin
- */
- function authorize() {
- let url = new URL(config.instance);
- url.pathname = "/oauth/authorize";
- url.searchParams.set("client_id", state.client_id);
- url.searchParams.set("redirect_uri", getCurrentUrl());
- url.searchParams.set("response_type", "code");
- url.searchParams.set("scope", config.scope.join(" "));
-
- window.location.assign(url.href);
- }
-
- function callback() {
- if (state.access_token != undefined) {
- return; // we're already done :)
- }
- let params = (new URL(window.location)).searchParams;
-
- let token = params.get("code");
- if (token != null) {
- console.log("got token callback:", token);
- }
-
- return authorizeToken(token)
- .catch((e) => {
- console.log("Error processing oauth callback:", e);
- logout(); // just to be sure
- });
- }
-
- function authorizeToken(token) {
- let url = new URL(config.instance);
- url.pathname = "/oauth/token";
- return fetch(url.href, {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- client_id: state.client_id,
- client_secret: state.client_secret,
- redirect_uri: getCurrentUrl(),
- grant_type: "authorization_code",
- code: token
- })
- }).then((res) => {
- if (res.status != 200) {
- throw res;
- }
- return res.json();
- }).then((json) => {
- state.access_token = json.access_token;
- storeState();
- window.location = getCurrentUrl(); // clear ?token=
- });
- }
-
- function isAuthorized() {
- return (state.access_token != undefined);
- }
-
- function apiRequest(path, method, data, type="json", accept="json") {
- if (!isAuthorized()) {
- throw new Error("Not Authenticated");
- }
- let url = new URL(config.instance);
- let [p, s] = path.split("?");
- url.pathname = p;
- if (s != undefined) {
- url.search = s;
- }
- let headers = {
- "Authorization": `Bearer ${state.access_token}`,
- "Accept": accept == "json" ? "application/json" : "*/*"
- };
- let body = data;
- if (type == "json" && body != undefined) {
- headers["Content-Type"] = "application/json";
- body = JSON.stringify(data);
- }
- return fetch(url.href, {
- method,
- headers,
- body
- }).then((res) => {
- return Promise.all([res.json(), res]);
- }).then(([json, res]) => {
- if (res.status != 200) {
- if (json.error) {
- throw new Error(json.error);
- } else {
- throw new Error(`${res.status}: ${res.statusText}`);
- }
- } else {
- return json;
- }
- }).catch(e => {
- if (e instanceof SyntaxError) {
- throw new Error("Error: The GtS API returned a non-json error. This usually means a network problem, or an issue with your instance's reverse proxy configuration.", {cause: e});
- } else {
- throw e;
- }
- });
- }
-
- function logout() {
- let url = new URL(config.instance);
- url.pathname = "/oauth/revoke";
- return fetch(url.href, {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- client_id: state.client_id,
- client_secret: state.client_secret,
- token: state.access_token,
- })
- }).then((res) => {
- if (res.status != 200) {
- // GoToSocial doesn't actually implement this route yet,
- // so error is to be expected
- return;
- }
- return res.json();
- }).catch(() => {
- // see above
- }).then(() => {
- localStorage.removeItem("oauth");
- window.location = getCurrentUrl();
- });
- }
-
- return {
- register, authorize, callback, isAuthorized, apiRequest, logout
- };
-};
diff --git a/web/source/panels/user/basic.js b/web/source/panels/user/basic.js
deleted file mode 100644
index f507b782b..000000000
--- a/web/source/panels/user/basic.js
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const React = require("react");
-const Promise = require("bluebird");
-
-const Submit = require("../../lib/submit");
-
-module.exports = function Basic({oauth, account, allowCustomCSS}) {
- const [errorMsg, setError] = React.useState("");
- const [statusMsg, setStatus] = React.useState("");
-
- const [headerFile, setHeaderFile] = React.useState(undefined);
- const [headerSrc, setHeaderSrc] = React.useState("");
-
- const [avatarFile, setAvatarFile] = React.useState(undefined);
- const [avatarSrc, setAvatarSrc] = React.useState("");
-
- const [displayName, setDisplayName] = React.useState("");
- const [bio, setBio] = React.useState("");
- const [locked, setLocked] = React.useState(false);
- const [customCSS, setCustomCSS] = React.useState("");
-
- React.useEffect(() => {
- setHeaderSrc(account.header);
- setAvatarSrc(account.avatar);
-
- setDisplayName(account.display_name);
- setBio(account.source ? account.source.note : "");
- setLocked(account.locked);
- setCustomCSS((allowCustomCSS && account.custom_css) ? account.custom_css : "");
- }, [account, setHeaderSrc, setAvatarSrc, setDisplayName, setBio, setLocked, setCustomCSS]);
-
- const headerOnChange = (e) => {
- setHeaderFile(e.target.files[0]);
- setHeaderSrc(URL.createObjectURL(e.target.files[0]));
- };
-
- const avatarOnChange = (e) => {
- setAvatarFile(e.target.files[0]);
- setAvatarSrc(URL.createObjectURL(e.target.files[0]));
- };
-
- const submit = (e) => {
- e.preventDefault();
-
- setStatus("PATCHing");
- setError("");
- return Promise.try(() => {
- let formDataInfo = new FormData();
-
- if (headerFile) {
- formDataInfo.set("header", headerFile);
- }
-
- if (avatarFile) {
- formDataInfo.set("avatar", avatarFile);
- }
-
- formDataInfo.set("display_name", displayName);
- formDataInfo.set("note", bio);
- formDataInfo.set("locked", locked);
-
- if (allowCustomCSS) {
- formDataInfo.set("custom_css", customCSS);
- }
-
- return oauth.apiRequest("/api/v1/accounts/update_credentials", "PATCH", formDataInfo, "form");
- }).then((json) => {
- setStatus("Saved!");
-
- setHeaderSrc(json.header);
- setAvatarSrc(json.avatar);
-
- setDisplayName(json.display_name);
- setBio(json.source.note);
- setLocked(json.locked);
- setCustomCSS(allowCustomCSS && json.custom_css ? json.custom_css : "");
- }).catch((e) => {
- setError(e.message);
- setStatus("");
- });
- };
-
- return (
-
-
@{account.username}'s Profile Info
-
-
- );
-};
diff --git a/web/source/panels/user/index.js b/web/source/panels/user/index.js
deleted file mode 100644
index aeecac415..000000000
--- a/web/source/panels/user/index.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-const React = require("react");
-const ReactDom = require("react-dom");
-
-const createPanel = require("../lib/panel");
-
-const Basic = require("./basic");
-const Posts = require("./posts");
-const Security = require("./security");
-
-require("../base.css");
-require("./style.css");
-
-function UserPanel({oauth}) {
- const [account, setAccount] = React.useState({});
- const [allowCustomCSS, setAllowCustomCSS] = React.useState(false);
- const [errorMsg, setError] = React.useState("");
- const [statusMsg, setStatus] = React.useState("Fetching user info");
-
- React.useEffect(() => {
-
- }, [oauth, setAllowCustomCSS, setError, setStatus]);
-
- React.useEffect(() => {
- Promise.try(() => {
- return oauth.apiRequest("/api/v1/instance", "GET");
- }).then((json) => {
- setAllowCustomCSS(json.configuration.accounts.allow_custom_css);
- Promise.try(() => {
- return oauth.apiRequest("/api/v1/accounts/verify_credentials", "GET");
- }).then((json) => {
- setAccount(json);
- }).catch((e) => {
- setError(e.message);
- setStatus("");
- });
- }).catch((e) => {
- setError(e.message);
- setStatus("");
- });
-
- }, [oauth, setAllowCustomCSS, setAccount, setError, setStatus]);
-
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-createPanel("GoToSocial User Panel", ["read write"], UserPanel);
\ No newline at end of file
diff --git a/web/source/panels/user/posts.js b/web/source/panels/user/posts.js
deleted file mode 100644
index e4ceae617..000000000
--- a/web/source/panels/user/posts.js
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const React = require("react");
-const Promise = require("bluebird");
-
-const Languages = require("./languages");
-const Submit = require("../../lib/submit");
-
-module.exports = function Posts({oauth, account}) {
- const [errorMsg, setError] = React.useState("");
- const [statusMsg, setStatus] = React.useState("");
-
- const [language, setLanguage] = React.useState("");
- const [privacy, setPrivacy] = React.useState("");
- const [format, setFormat] = React.useState("");
- const [sensitive, setSensitive] = React.useState(false);
-
- React.useEffect(() => {
- if (account.source) {
- setLanguage(account.source.language.toUpperCase());
- setPrivacy(account.source.privacy);
- setSensitive(account.source.sensitive ? account.source.sensitive : false);
- setFormat(account.source.status_format ? account.source.status_format : "plain");
- }
-
- }, [account, setSensitive, setPrivacy]);
-
- const submit = (e) => {
- e.preventDefault();
-
- setStatus("PATCHing");
- setError("");
- return Promise.try(() => {
- let formDataInfo = new FormData();
-
- formDataInfo.set("source[language]", language);
- formDataInfo.set("source[privacy]", privacy);
- formDataInfo.set("source[sensitive]", sensitive);
- formDataInfo.set("source[status_format]", format);
-
- return oauth.apiRequest("/api/v1/accounts/update_credentials", "PATCH", formDataInfo, "form");
- }).then((json) => {
- setStatus("Saved!");
- setLanguage(json.source.language.toUpperCase());
- setPrivacy(json.source.privacy);
- setSensitive(json.source.sensitive ? json.source.sensitive : false);
- setFormat(json.source.status_format ? json.source.status_format : "plain");
- }).catch((e) => {
- setError(e.message);
- setStatus("");
- });
- };
-
- return (
-
-
Post Settings
-
-
- );
-};
diff --git a/web/source/panels/user/security.js b/web/source/panels/user/security.js
deleted file mode 100644
index f5925083d..000000000
--- a/web/source/panels/user/security.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-"use strict";
-
-const React = require("react");
-const Promise = require("bluebird");
-
-const Submit = require("../../lib/submit");
-
-module.exports = function Security({oauth}) {
- const [errorMsg, setError] = React.useState("");
- const [statusMsg, setStatus] = React.useState("");
-
- const [oldPassword, setOldPassword] = React.useState("");
- const [newPassword, setNewPassword] = React.useState("");
- const [newPasswordConfirm, setNewPasswordConfirm] = React.useState("");
-
- const submit = (e) => {
- e.preventDefault();
-
- if (newPassword !== newPasswordConfirm) {
- setError("New password and confirm new password did not match!");
- return;
- }
-
- setStatus("PATCHing");
- setError("");
- return Promise.try(() => {
- let formDataInfo = new FormData();
- formDataInfo.set("old_password", oldPassword);
- formDataInfo.set("new_password", newPassword);
- return oauth.apiRequest("/api/v1/user/password_change", "POST", formDataInfo, "form");
- }).then((json) => {
- setStatus("Saved!");
- setOldPassword("");
- setNewPassword("");
- setNewPasswordConfirm("");
- }).catch((e) => {
- setError(e.message);
- setStatus("");
- });
- };
-
- return (
-
-
Password Change
-
-
- );
-};
diff --git a/web/source/panels/user/style.css b/web/source/panels/user/style.css
deleted file mode 100644
index 021b1816e..000000000
--- a/web/source/panels/user/style.css
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-section.basic, section.posts, section.security {
- form {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-
- input, textarea {
- width: 100%;
- line-height: 1.5rem;
- }
-
- input[type=checkbox] {
- justify-self: start;
- width: initial;
- }
-
- input:read-only {
- border: none;
- }
-
- input:invalid {
- border-color: red;
- }
- }
-
- textarea {
- width: 100%;
- height: 8rem;
- }
-
- h1 {
- margin-bottom: 0.5rem;
- }
-
- img {
- display: flex;
- justify-content: center;
- align-items: center;
- border: $boxshadow_border;
- box-shadow: $box-shadow;
- object-fit: cover;
- border-radius: 0.2rem;
- box-sizing: border-box;
- margin-bottom: 0.5rem;
- }
-
- .avatarpreview {
- height: 8.5rem;
- width: 8.5rem;
- }
-
- .headerpreview {
- width: 100%;
- aspect-ratio: 3 / 1;
- overflow: hidden;
- }
-
- .moreinfolink {
- font-size: 0.9em;
- }
-}
-
-.labelinput .border {
- border-radius: 0.2rem;
- border: 0.15rem solid $border_accent;
- padding: 0.3rem;
- display: flex;
- flex-direction: column;
-}
-
-.file-input.button {
- display: inline-block;
- font-size: 1rem;
- font-weight: normal;
- padding: 0.3rem 0.3rem;
- align-self: flex-start;
- /* background: $border_accent; */
- margin-right: 0.2rem;
-}
-
-.labelinput, .labelselect {
- display: flex;
- flex-direction: column;
- gap: 0.4rem;
-}
-
-.labelcheckbox {
- display: flex;
- gap: 0.4rem;
-}
-
-.titlesave {
- display: flex;
- flex-wrap: wrap;
- gap: 0.4rem;
-}
-
-.logout {
- margin-bottom: 2rem;
-}
diff --git a/web/source/settings-panel/admin/actions.js b/web/source/settings-panel/admin/actions.js
new file mode 100644
index 000000000..d4980d021
--- /dev/null
+++ b/web/source/settings-panel/admin/actions.js
@@ -0,0 +1,61 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+
+const Submit = require("../components/submit");
+
+const api = require("../lib/api");
+const submit = require("../lib/submit");
+
+module.exports = function AdminActionPanel() {
+ const dispatch = Redux.useDispatch();
+
+ const [days, setDays] = React.useState(30);
+
+ const [errorMsg, setError] = React.useState("");
+ const [statusMsg, setStatus] = React.useState("");
+
+ const removeMedia = submit(
+ () => dispatch(api.admin.mediaCleanup(days)),
+ {setStatus, setError}
+ );
+
+ return (
+ <>
+
Admin Actions
+
+
Media cleanup
+
+ Clean up remote media older than the specified number of days.
+ If the remote instance is still online they will be refetched when needed.
+ Also cleans up unused headers and avatars from the media cache.
+
+
+
+ setDays(e.target.value)}/>
+
+
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/admin/emoji.js b/web/source/settings-panel/admin/emoji.js
new file mode 100644
index 000000000..1ef4a54a3
--- /dev/null
+++ b/web/source/settings-panel/admin/emoji.js
@@ -0,0 +1,212 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter");
+
+const Submit = require("../components/submit");
+const FakeToot = require("../components/fake-toot");
+const { formFields } = require("../components/form-fields");
+
+const api = require("../lib/api");
+const adminActions = require("../redux/reducers/admin").actions;
+const submit = require("../lib/submit");
+
+const base = "/settings/admin/custom-emoji";
+
+module.exports = function CustomEmoji() {
+ return (
+
+
+
+
+
+
+ );
+};
+
+function EmojiOverview() {
+ const dispatch = Redux.useDispatch();
+ const [loaded, setLoaded] = React.useState(false);
+
+ const [errorMsg, setError] = React.useState("");
+
+ React.useEffect(() => {
+ if (!loaded) {
+ Promise.try(() => {
+ return dispatch(api.admin.fetchCustomEmoji());
+ }).then(() => {
+ setLoaded(true);
+ }).catch((e) => {
+ setLoaded(true);
+ setError(e.message);
+ });
+ }
+ }, []);
+
+ if (!loaded) {
+ return (
+ <>
+
+ );
+}
+
+function EmojiDetailWrapped() {
+ /* We wrap the component to generate formFields with a setter depending on the domain
+ if formFields() is used inside the same component that is re-rendered with their state,
+ inputs get re-created on every change, causing them to lose focus, and bad performance
+ */
+ let [_match, {emojiId}] = useRoute(`${base}/:emojiId`);
+
+ function alterEmoji([key, val]) {
+ return adminActions.updateDomainBlockVal([emojiId, key, val]);
+ }
+
+ const fields = formFields(alterEmoji, (state) => state.admin.blockedInstances[emojiId]);
+
+ return ;
+}
+
+function EmojiDetail({id, Form}) {
+ return (
+ "Not implemented yet"
+ );
+}
\ No newline at end of file
diff --git a/web/source/settings-panel/admin/federation.js b/web/source/settings-panel/admin/federation.js
new file mode 100644
index 000000000..7afc3c699
--- /dev/null
+++ b/web/source/settings-panel/admin/federation.js
@@ -0,0 +1,382 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter");
+const fileDownload = require("js-file-download");
+
+const { formFields } = require("../components/form-fields");
+
+const api = require("../lib/api");
+const adminActions = require("../redux/reducers/admin").actions;
+const submit = require("../lib/submit");
+
+const base = "/settings/admin/federation";
+
+// const {
+// TextInput,
+// TextArea,
+// File
+// } = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings);
+
+module.exports = function AdminSettings() {
+ const dispatch = Redux.useDispatch();
+ // const instance = Redux.useSelector(state => state.instances.adminSettings);
+ const loadedBlockedInstances = Redux.useSelector(state => state.admin.loadedBlockedInstances);
+
+ React.useEffect(() => {
+ if (!loadedBlockedInstances ) {
+ Promise.try(() => {
+ return dispatch(api.admin.fetchDomainBlocks());
+ });
+ }
+ }, []);
+
+ if (!loadedBlockedInstances) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/web/source/settings-panel/admin/settings.js b/web/source/settings-panel/admin/settings.js
new file mode 100644
index 000000000..845a1f924
--- /dev/null
+++ b/web/source/settings-panel/admin/settings.js
@@ -0,0 +1,110 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+
+const Submit = require("../components/submit");
+
+const api = require("../lib/api");
+const submit = require("../lib/submit");
+
+const adminActions = require("../redux/reducers/instances").actions;
+
+const {
+ TextInput,
+ TextArea,
+ File
+} = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings);
+
+module.exports = function AdminSettings() {
+ const dispatch = Redux.useDispatch();
+
+ const [errorMsg, setError] = React.useState("");
+ const [statusMsg, setStatus] = React.useState("");
+
+ const updateSettings = submit(
+ () => dispatch(api.admin.updateInstance()),
+ {setStatus, setError}
+ );
+
+ return (
+
+
Instance Settings
+
+
+
+
+
+
+
+
+
+
+ {/*
+
Instance avatar
+
+
+
+
+
+
+
+
Instance header
+
+
+
+
+
*/}
+
+
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/components/error.jsx b/web/source/settings-panel/components/error.jsx
new file mode 100644
index 000000000..13dc686b7
--- /dev/null
+++ b/web/source/settings-panel/components/error.jsx
@@ -0,0 +1,45 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+
+module.exports = function ErrorFallback({error, resetErrorBoundary}) {
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/components/fake-toot.jsx b/web/source/settings-panel/components/fake-toot.jsx
new file mode 100644
index 000000000..f79e24eb9
--- /dev/null
+++ b/web/source/settings-panel/components/fake-toot.jsx
@@ -0,0 +1,43 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const React = require("react");
+const Redux = require("react-redux");
+
+module.exports = function FakeToot({children}) {
+ const account = Redux.useSelector((state) => state.user.profile);
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/components/form-fields.jsx b/web/source/settings-panel/components/form-fields.jsx
new file mode 100644
index 000000000..cb402c3b2
--- /dev/null
+++ b/web/source/settings-panel/components/form-fields.jsx
@@ -0,0 +1,167 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const React = require("react");
+const Redux = require("react-redux");
+const d = require("dotty");
+const prettierBytes = require("prettier-bytes");
+
+function eventListeners(dispatch, setter, obj) {
+ return {
+ onTextChange: function (key) {
+ return function (e) {
+ dispatch(setter([key, e.target.value]));
+ };
+ },
+
+ onCheckChange: function (key) {
+ return function (e) {
+ dispatch(setter([key, e.target.checked]));
+ };
+ },
+
+ onFileChange: function (key, withPreview) {
+ return function (e) {
+ let file = e.target.files[0];
+ if (withPreview) {
+ let old = d.get(obj, key);
+ if (old != undefined) {
+ URL.revokeObjectURL(old); // no error revoking a non-Object URL as provided by instance
+ }
+ let objectURL = URL.createObjectURL(file);
+ dispatch(setter([key, objectURL]));
+ }
+ dispatch(setter([`${key}File`, file]));
+ };
+ }
+ };
+}
+
+function get(state, id, defaultVal) {
+ let value;
+ if (id.includes(".")) {
+ value = d.get(state, id);
+ } else {
+ value = state[id];
+ }
+ if (value == undefined) {
+ value = defaultVal;
+ }
+ return value;
+}
+
+// function removeFile(name) {
+// return function(e) {
+// e.preventDefault();
+// dispatch(user.setProfileVal([name, ""]));
+// dispatch(user.setProfileVal([`${name}File`, ""]));
+// };
+// }
+
+module.exports = {
+ formFields: function formFields(setter, selector) {
+ function FormField({
+ type, id, name, className="", placeHolder="", fileType="", children=null,
+ options=null, inputProps={}, withPreview=true, showSize=false, maxSize=Infinity
+ }) {
+ const dispatch = Redux.useDispatch();
+ let state = Redux.useSelector(selector);
+ let {
+ onTextChange,
+ onCheckChange,
+ onFileChange
+ } = eventListeners(dispatch, setter, state);
+
+ let field;
+ let defaultLabel = true;
+ if (type == "text") {
+ field = ;
+ } else if (type == "textarea") {
+ field = ;
+ } else if (type == "checkbox") {
+ field = ;
+ } else if (type == "select") {
+ field = (
+
+ );
+ } else if (type == "file") {
+ defaultLabel = false;
+ let file = get(state, `${id}File`);
+
+ let size = null;
+ if (showSize && file) {
+ size = `(${prettierBytes(file.size)})`;
+
+ if (file.size > maxSize) {
+ size = {size};
+ }
+ }
+
+ field = (
+ <>
+
+
+ {file ? file.name : "no file selected"} {size}
+
+ {/* remove */}
+
+ >
+ );
+ } else {
+ defaultLabel = false;
+ field = `unsupported FormField ${type}, this is a developer error`;
+ }
+
+ let label = ;
+ return (
+
+ );
+ }
+
+ return {
+ TextInput: function(props) {
+ return ;
+ },
+
+ TextArea: function(props) {
+ return ;
+ },
+
+ Checkbox: function(props) {
+ return ;
+ },
+
+ Select: function(props) {
+ return ;
+ },
+
+ File: function(props) {
+ return ;
+ },
+ };
+ },
+
+ eventListeners
+};
\ No newline at end of file
diff --git a/web/source/panels/user/languages.js b/web/source/settings-panel/components/languages.jsx
similarity index 79%
rename from web/source/panels/user/languages.js
rename to web/source/settings-panel/components/languages.jsx
index c20e08426..1522495da 100644
--- a/web/source/panels/user/languages.js
+++ b/web/source/settings-panel/components/languages.jsx
@@ -1,19 +1,19 @@
/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
*/
"use strict";
diff --git a/web/source/settings-panel/components/login.jsx b/web/source/settings-panel/components/login.jsx
new file mode 100644
index 000000000..c67e99acd
--- /dev/null
+++ b/web/source/settings-panel/components/login.jsx
@@ -0,0 +1,102 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+
+const { setInstance } = require("../redux/reducers/oauth").actions;
+const api = require("../lib/api");
+
+module.exports = function Login({error}) {
+ const dispatch = Redux.useDispatch();
+ const [ instanceField, setInstanceField ] = React.useState("");
+ const [ errorMsg, setErrorMsg ] = React.useState();
+ const instanceFieldRef = React.useRef("");
+
+ React.useEffect(() => {
+ // check if current domain runs an instance
+ let currentDomain = window.location.origin;
+ Promise.try(() => {
+ return dispatch(api.instance.fetchWithoutStore(currentDomain));
+ }).then(() => {
+ if (instanceFieldRef.current.length == 0) { // user hasn't started typing yet
+ dispatch(setInstance(currentDomain));
+ instanceFieldRef.current = currentDomain;
+ setInstanceField(currentDomain);
+ }
+ }).catch((e) => {
+ console.log("Current domain does not host a valid instance: ", e);
+ });
+ }, []);
+
+ function tryInstance() {
+ let domain = instanceFieldRef.current;
+ Promise.try(() => {
+ return dispatch(api.instance.fetchWithoutStore(domain)).catch((e) => {
+ // TODO: clearer error messages for common errors
+ console.log(e);
+ throw e;
+ });
+ }).then(() => {
+ dispatch(setInstance(domain));
+
+ return dispatch(api.oauth.register()).catch((e) => {
+ console.log(e);
+ throw e;
+ });
+ }).then(() => {
+ return dispatch(api.oauth.authorize()); // will send user off-page
+ }).catch((e) => {
+ setErrorMsg(
+ <>
+ {e.type}
+ {e.message}
+ >
+ );
+ });
+ }
+
+ function updateInstanceField(e) {
+ if (e.key == "Enter") {
+ tryInstance(instanceField);
+ } else {
+ setInstanceField(e.target.value);
+ instanceFieldRef.current = e.target.value;
+ }
+ }
+
+ return (
+
+
OAUTH Login:
+ {error}
+
+
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/components/nav-button.jsx b/web/source/settings-panel/components/nav-button.jsx
new file mode 100644
index 000000000..3c76711fb
--- /dev/null
+++ b/web/source/settings-panel/components/nav-button.jsx
@@ -0,0 +1,33 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const React = require("react");
+const { Link, useRoute } = require("wouter");
+
+module.exports = function NavButton({href, name}) {
+ const [isActive] = useRoute(`${href}/:anything?`);
+ return (
+
+
+ {name}
+
+
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/components/submit.jsx b/web/source/settings-panel/components/submit.jsx
new file mode 100644
index 000000000..0187fc81f
--- /dev/null
+++ b/web/source/settings-panel/components/submit.jsx
@@ -0,0 +1,35 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const React = require("react");
+
+module.exports = function Submit({onClick, label, errorMsg, statusMsg}) {
+ return (
+
+
+ {errorMsg.length > 0 &&
+
{errorMsg}
+ }
+ {statusMsg.length > 0 &&
+
{statusMsg}
+ }
+
+ );
+};
diff --git a/web/source/settings-panel/index.js b/web/source/settings-panel/index.js
new file mode 100644
index 000000000..34720e818
--- /dev/null
+++ b/web/source/settings-panel/index.js
@@ -0,0 +1,178 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+const ReactDom = require("react-dom/client");
+const Redux = require("react-redux");
+const { Switch, Route, Redirect } = require("wouter");
+const { Provider } = require("react-redux");
+const { PersistGate } = require("redux-persist/integration/react");
+
+const { store, persistor } = require("./redux");
+const api = require("./lib/api");
+const oauth = require("./redux/reducers/oauth").actions;
+const { AuthenticationError } = require("./lib/errors");
+
+const Login = require("./components/login");
+
+require("./style.css");
+
+// TODO: nested categories?
+const nav = {
+ "User": {
+ "Profile": require("./user/profile.js"),
+ "Settings": require("./user/settings.js"),
+ },
+ "Admin": {
+ adminOnly: true,
+ "Instance Settings": require("./admin/settings.js"),
+ "Actions": require("./admin/actions"),
+ "Federation": require("./admin/federation.js"),
+ "Custom Emoji": require("./admin/emoji.js"),
+ }
+};
+
+const { sidebar, panelRouter } = require("./lib/get-views")(nav);
+
+function App() {
+ const dispatch = Redux.useDispatch();
+
+ const { loginState, isAdmin } = Redux.useSelector((state) => state.oauth);
+ const reduxTempStatus = Redux.useSelector((state) => state.temporary.status);
+
+ const [errorMsg, setErrorMsg] = React.useState();
+ const [tokenChecked, setTokenChecked] = React.useState(false);
+
+ React.useEffect(() => {
+ if (loginState == "login" || loginState == "callback") {
+ Promise.try(() => {
+ // Process OAUTH authorization token from URL if available
+ if (loginState == "callback") {
+ let urlParams = new URLSearchParams(window.location.search);
+ let code = urlParams.get("code");
+
+ if (code == undefined) {
+ setErrorMsg(new Error("Waiting for OAUTH callback but no ?code= provided. You can try logging in again:"));
+ } else {
+ return dispatch(api.oauth.tokenize(code));
+ }
+ }
+ }).then(() => {
+ // Fetch current instance info
+ return dispatch(api.instance.fetch());
+ }).then(() => {
+ // Check currently stored auth token for validity if available
+ return dispatch(api.user.fetchAccount());
+ }).then(() => {
+ setTokenChecked(true);
+
+ return dispatch(api.oauth.checkIfAdmin());
+ }).catch((e) => {
+ if (e instanceof AuthenticationError) {
+ dispatch(oauth.remove());
+ e.message = "Stored OAUTH token no longer valid, please log in again.";
+ }
+ setErrorMsg(e);
+ console.error(e);
+ });
+ }
+ }, []);
+
+ let ErrorElement = null;
+ if (errorMsg != undefined) {
+ ErrorElement = (
+
+ {ErrorElement}
+ {LogoutElement}
+
+ );
+ }
+
+}
+
+function Main() {
+ return (
+
+
+
+
+
+ );
+}
+
+const root = ReactDom.createRoot(document.getElementById("root"));
+root.render();
\ No newline at end of file
diff --git a/web/source/settings-panel/lib/api/admin.js b/web/source/settings-panel/lib/api/admin.js
new file mode 100644
index 000000000..1df47b693
--- /dev/null
+++ b/web/source/settings-panel/lib/api/admin.js
@@ -0,0 +1,192 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const isValidDomain = require("is-valid-domain");
+
+const instance = require("../../redux/reducers/instances").actions;
+const admin = require("../../redux/reducers/admin").actions;
+
+module.exports = function ({ apiCall, getChanges }) {
+ const adminAPI = {
+ updateInstance: function updateInstance() {
+ return function (dispatch, getState) {
+ return Promise.try(() => {
+ const state = getState().instances.adminSettings;
+
+ const update = getChanges(state, {
+ formKeys: ["title", "short_description", "description", "contact_account.username", "email", "terms"],
+ renamedKeys: {"contact_account.username": "contact_username"},
+ // fileKeys: ["avatar", "header"]
+ });
+
+ return dispatch(apiCall("PATCH", "/api/v1/instance", update, "form"));
+ }).then((data) => {
+ return dispatch(instance.setInstanceInfo(data));
+ });
+ };
+ },
+
+ fetchDomainBlocks: function fetchDomainBlocks() {
+ return function (dispatch, _getState) {
+ return Promise.try(() => {
+ return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks"));
+ }).then((data) => {
+ return dispatch(admin.setBlockedInstances(data));
+ });
+ };
+ },
+
+ updateDomainBlock: function updateDomainBlock(domain) {
+ return function (dispatch, getState) {
+ return Promise.try(() => {
+ const state = getState().admin.newInstanceBlocks[domain];
+ const update = getChanges(state, {
+ formKeys: ["domain", "obfuscate", "public_comment", "private_comment"],
+ });
+
+ return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks", update, "form"));
+ }).then((block) => {
+ return Promise.all([
+ dispatch(admin.newDomainBlock([domain, block])),
+ dispatch(admin.setDomainBlock([domain, block]))
+ ]);
+ });
+ };
+ },
+
+ getEditableDomainBlock: function getEditableDomainBlock(domain) {
+ return function (dispatch, getState) {
+ let data = getState().admin.blockedInstances[domain];
+ return dispatch(admin.newDomainBlock([domain, data]));
+ };
+ },
+
+ bulkDomainBlock: function bulkDomainBlock() {
+ return function (dispatch, getState) {
+ let invalidDomains = [];
+ let success = 0;
+
+ return Promise.try(() => {
+ const state = getState().admin.bulkBlock;
+ let list = state.list;
+ let domains;
+
+ let fields = getChanges(state, {
+ formKeys: ["obfuscate", "public_comment", "private_comment"]
+ });
+
+ let defaultDate = new Date().toUTCString();
+
+ if (list[0] == "[") {
+ domains = JSON.parse(state.list);
+ } else {
+ domains = list.split("\n").map((line_) => {
+ let line = line_.trim();
+ if (line.length == 0) {
+ return null;
+ }
+
+ if (!isValidDomain(line, {wildcard: true, allowUnicode: true})) {
+ invalidDomains.push(line);
+ return null;
+ }
+
+ return {
+ domain: line,
+ created_at: defaultDate,
+ ...fields
+ };
+ }).filter((a) => a != null);
+ }
+
+ if (domains.length == 0) {
+ return;
+ }
+
+ const update = {
+ domains: new Blob([JSON.stringify(domains)], {type: "application/json"})
+ };
+
+ return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks?import=true", update, "form"));
+ }).then((blocks) => {
+ if (blocks != undefined) {
+ return Promise.each(blocks, (block) => {
+ success += 1;
+ return dispatch(admin.setDomainBlock([block.domain, block]));
+ });
+ }
+ }).then(() => {
+ return {
+ success,
+ invalidDomains
+ };
+ });
+ };
+ },
+
+ removeDomainBlock: function removeDomainBlock(domain) {
+ return function (dispatch, getState) {
+ return Promise.try(() => {
+ const id = getState().admin.blockedInstances[domain].id;
+ return dispatch(apiCall("DELETE", `/api/v1/admin/domain_blocks/${id}`));
+ }).then((removed) => {
+ return dispatch(admin.removeDomainBlock(removed.domain));
+ });
+ };
+ },
+
+ mediaCleanup: function mediaCleanup(days) {
+ return function (dispatch, _getState) {
+ return Promise.try(() => {
+ return dispatch(apiCall("POST", `/api/v1/admin/media_cleanup?remote_cache_days=${days}`));
+ });
+ };
+ },
+
+ fetchCustomEmoji: function fetchCustomEmoji() {
+ return function (dispatch, _getState) {
+ return Promise.try(() => {
+ return dispatch(apiCall("GET", "/api/v1/custom_emojis"));
+ }).then((emoji) => {
+ return dispatch(admin.setEmoji(emoji));
+ });
+ };
+ },
+
+ newEmoji: function newEmoji() {
+ return function (dispatch, getState) {
+ return Promise.try(() => {
+ const state = getState().admin.newEmoji;
+
+ const update = getChanges(state, {
+ formKeys: ["shortcode"],
+ fileKeys: ["image"]
+ });
+
+ return dispatch(apiCall("POST", "/api/v1/admin/custom_emojis", update, "form"));
+ }).then((emoji) => {
+ return dispatch(admin.addEmoji(emoji));
+ });
+ };
+ }
+ };
+ return adminAPI;
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/lib/api/index.js b/web/source/settings-panel/lib/api/index.js
new file mode 100644
index 000000000..e699011bd
--- /dev/null
+++ b/web/source/settings-panel/lib/api/index.js
@@ -0,0 +1,185 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const { isPlainObject } = require("is-plain-object");
+const d = require("dotty");
+
+const { APIError, AuthenticationError } = require("../errors");
+const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions;
+const oauth = require("../../redux/reducers/oauth").actions;
+
+function apiCall(method, route, payload, type = "json") {
+ return function (dispatch, getState) {
+ const state = getState();
+ let base = state.oauth.instance;
+ let auth = state.oauth.token;
+ console.log(method, base, route, "auth:", auth != undefined);
+
+ return Promise.try(() => {
+ let url = new URL(base);
+ let [path, query] = route.split("?");
+ url.pathname = path;
+ if (query != undefined) {
+ url.search = query;
+ }
+ let body;
+
+ let headers = {
+ "Accept": "application/json",
+ };
+
+ if (payload != undefined) {
+ if (type == "json") {
+ headers["Content-Type"] = "application/json";
+ body = JSON.stringify(payload);
+ } else if (type == "form") {
+ const formData = new FormData();
+ Object.entries(payload).forEach(([key, val]) => {
+ if (isPlainObject(val)) {
+ Object.entries(val).forEach(([key2, val2]) => {
+ if (val2 != undefined) {
+ formData.set(`${key}[${key2}]`, val2);
+ }
+ });
+ } else {
+ if (val != undefined) {
+ formData.set(key, val);
+ }
+ }
+ });
+ body = formData;
+ }
+ }
+
+ if (auth != undefined) {
+ headers["Authorization"] = auth;
+ }
+
+ return fetch(url.toString(), {
+ method,
+ headers,
+ body
+ });
+ }).then((res) => {
+ // try parse json even with error
+ let json = res.json().catch((e) => {
+ throw new APIError(`JSON parsing error: ${e.message}`);
+ });
+
+ return Promise.all([res, json]);
+ }).then(([res, json]) => {
+ if (!res.ok) {
+ if (auth != undefined && (res.status == 401 || res.status == 403)) {
+ // stored access token is invalid
+ throw new AuthenticationError("401: Authentication error", {json, status: res.status});
+ } else {
+ throw new APIError(json.error, { json });
+ }
+ } else {
+ return json;
+ }
+ });
+ };
+}
+
+function getChanges(state, keys) {
+ const { formKeys = [], fileKeys = [], renamedKeys = {} } = keys;
+ const update = {};
+
+ formKeys.forEach((key) => {
+ let value = d.get(state, key);
+ if (value == undefined) {
+ return;
+ }
+ if (renamedKeys[key]) {
+ key = renamedKeys[key];
+ }
+ d.put(update, key, value);
+ });
+
+ fileKeys.forEach((key) => {
+ let file = d.get(state, `${key}File`);
+ if (file != undefined) {
+ if (renamedKeys[key]) {
+ key = renamedKeys[key];
+ }
+ d.put(update, key, file);
+ }
+ });
+
+ return update;
+}
+
+function getCurrentUrl() {
+ return `${window.location.origin}${window.location.pathname}`;
+}
+
+function fetchInstanceWithoutStore(domain) {
+ return function (dispatch, getState) {
+ return Promise.try(() => {
+ let lookup = getState().instances.info[domain];
+ if (lookup != undefined) {
+ return lookup;
+ }
+
+ // apiCall expects to pull the domain from state,
+ // but we don't want to store it there yet
+ // so we mock the API here with our function argument
+ let fakeState = {
+ oauth: { instance: domain }
+ };
+
+ return apiCall("GET", "/api/v1/instance")(dispatch, () => fakeState);
+ }).then((json) => {
+ if (json && json.uri) { // TODO: validate instance json more?
+ dispatch(setNamedInstanceInfo([domain, json]));
+ return json;
+ }
+ });
+ };
+}
+
+function fetchInstance() {
+ return function (dispatch, _getState) {
+ return Promise.try(() => {
+ return dispatch(apiCall("GET", "/api/v1/instance"));
+ }).then((json) => {
+ if (json && json.uri) {
+ dispatch(setInstanceInfo(json));
+ return json;
+ }
+ });
+ };
+}
+
+let submoduleArgs = { apiCall, getCurrentUrl, getChanges };
+
+module.exports = {
+ instance: {
+ fetchWithoutStore: fetchInstanceWithoutStore,
+ fetch: fetchInstance
+ },
+ oauth: require("./oauth")(submoduleArgs),
+ user: require("./user")(submoduleArgs),
+ admin: require("./admin")(submoduleArgs),
+ apiCall,
+ getChanges
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/lib/api/oauth.js b/web/source/settings-panel/lib/api/oauth.js
new file mode 100644
index 000000000..76d0e9d2f
--- /dev/null
+++ b/web/source/settings-panel/lib/api/oauth.js
@@ -0,0 +1,124 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+
+const { OAUTHError, AuthenticationError } = require("../errors");
+
+const oauth = require("../../redux/reducers/oauth").actions;
+const temporary = require("../../redux/reducers/temporary").actions;
+const admin = require("../../redux/reducers/admin").actions;
+
+module.exports = function oauthAPI({ apiCall, getCurrentUrl }) {
+ return {
+
+ register: function register(scopes = []) {
+ return function (dispatch, _getState) {
+ return Promise.try(() => {
+ return dispatch(apiCall("POST", "/api/v1/apps", {
+ client_name: "GoToSocial Settings",
+ scopes: scopes.join(" "),
+ redirect_uris: getCurrentUrl(),
+ website: getCurrentUrl()
+ }));
+ }).then((json) => {
+ json.scopes = scopes;
+ dispatch(oauth.setRegistration(json));
+ });
+ };
+ },
+
+ authorize: function authorize() {
+ return function (dispatch, getState) {
+ let state = getState();
+ let reg = state.oauth.registration;
+ let base = new URL(state.oauth.instance);
+
+ base.pathname = "/oauth/authorize";
+ base.searchParams.set("client_id", reg.client_id);
+ base.searchParams.set("redirect_uri", getCurrentUrl());
+ base.searchParams.set("response_type", "code");
+ base.searchParams.set("scope", reg.scopes.join(" "));
+
+ dispatch(oauth.setLoginState("callback"));
+ dispatch(temporary.setStatus("Redirecting to instance login..."));
+
+ // send user to instance's login flow
+ window.location.assign(base.href);
+ };
+ },
+
+ tokenize: function tokenize(code) {
+ return function (dispatch, getState) {
+ let reg = getState().oauth.registration;
+
+ return Promise.try(() => {
+ if (reg == undefined || reg.client_id == undefined) {
+ throw new OAUTHError("Callback code present, but no client registration is available from localStorage. \nNote: localStorage is unavailable in Private Browsing.");
+ }
+
+ return dispatch(apiCall("POST", "/oauth/token", {
+ client_id: reg.client_id,
+ client_secret: reg.client_secret,
+ redirect_uri: getCurrentUrl(),
+ grant_type: "authorization_code",
+ code: code
+ }));
+ }).then((json) => {
+ window.history.replaceState({}, document.title, window.location.pathname);
+ return dispatch(oauth.login(json));
+ });
+ };
+ },
+
+ checkIfAdmin: function checkIfAdmin() {
+ return function (dispatch, getState) {
+ const state = getState();
+ let stored = state.oauth.isAdmin;
+ if (stored != undefined) {
+ return stored;
+ }
+
+ // newer GoToSocial version will include a `role` in the Account data, check that first
+ // TODO: check account data for admin status
+
+ // no role info, try fetching an admin-only route and see if we get an error
+ return Promise.try(() => {
+ return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks"));
+ }).then((data) => {
+ return Promise.all([
+ dispatch(oauth.setAdmin(true)),
+ dispatch(admin.setBlockedInstances(data))
+ ]);
+ }).catch(AuthenticationError, () => {
+ return dispatch(oauth.setAdmin(false));
+ });
+ };
+ },
+
+ logout: function logout() {
+ return function (dispatch, _getState) {
+ // TODO: GoToSocial does not have a logout API route yet
+
+ return dispatch(oauth.remove());
+ };
+ }
+ };
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/lib/api/user.js b/web/source/settings-panel/lib/api/user.js
new file mode 100644
index 000000000..18b54bd73
--- /dev/null
+++ b/web/source/settings-panel/lib/api/user.js
@@ -0,0 +1,67 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+
+const user = require("../../redux/reducers/user").actions;
+
+module.exports = function ({ apiCall, getChanges }) {
+ function updateCredentials(selector, keys) {
+ return function (dispatch, getState) {
+ return Promise.try(() => {
+ const state = selector(getState());
+
+ const update = getChanges(state, keys);
+
+ return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
+ }).then((account) => {
+ return dispatch(user.setAccount(account));
+ });
+ };
+ }
+
+ return {
+ fetchAccount: function fetchAccount() {
+ return function (dispatch, _getState) {
+ return Promise.try(() => {
+ return dispatch(apiCall("GET", "/api/v1/accounts/verify_credentials"));
+ }).then((account) => {
+ return dispatch(user.setAccount(account));
+ });
+ };
+ },
+
+ updateProfile: function updateProfile() {
+ const formKeys = ["display_name", "locked", "source", "custom_css", "source.note"];
+ const renamedKeys = {
+ "source.note": "note"
+ };
+ const fileKeys = ["header", "avatar"];
+
+ return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys});
+ },
+
+ updateSettings: function updateProfile() {
+ const formKeys = ["source"];
+
+ return updateCredentials((state) => state.user.settings, {formKeys});
+ }
+ };
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/lib/errors.js b/web/source/settings-panel/lib/errors.js
new file mode 100644
index 000000000..c2f781cb2
--- /dev/null
+++ b/web/source/settings-panel/lib/errors.js
@@ -0,0 +1,27 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const createError = require("create-error");
+
+module.exports = {
+ APIError: createError("APIError"),
+ OAUTHError: createError("OAUTHError"),
+ AuthenticationError: createError("AuthenticationError"),
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/lib/get-views.js b/web/source/settings-panel/lib/get-views.js
new file mode 100644
index 000000000..39f627435
--- /dev/null
+++ b/web/source/settings-panel/lib/get-views.js
@@ -0,0 +1,102 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const React = require("react");
+const Redux = require("react-redux");
+const { Link, Route, Switch, Redirect } = require("wouter");
+const { ErrorBoundary } = require("react-error-boundary");
+
+const ErrorFallback = require("../components/error");
+const NavButton = require("../components/nav-button");
+
+function urlSafe(str) {
+ return str.toLowerCase().replace(/\s+/g, "-");
+}
+
+module.exports = function getViews(struct) {
+ const sidebar = {
+ all: [],
+ admin: [],
+ };
+
+ const panelRouter = {
+ all: [],
+ admin: [],
+ };
+
+ Object.entries(struct).forEach(([name, entries]) => {
+ let sidebarEl = sidebar.all;
+ let panelRouterEl = panelRouter.all;
+
+ if (entries.adminOnly) {
+ sidebarEl = sidebar.admin;
+ panelRouterEl = panelRouter.admin;
+ delete entries.adminOnly;
+ }
+
+ let base = `/settings/${urlSafe(name)}`;
+
+ let links = [];
+
+ let firstRoute;
+
+ Object.entries(entries).forEach(([name, ViewComponent]) => {
+ let url = `${base}/${urlSafe(name)}`;
+
+ if (firstRoute == undefined) {
+ firstRoute = url;
+ }
+
+ panelRouterEl.push((
+
+ { }}>
+ {/* FIXME: implement onReset */}
+
+
+
+ ));
+
+ links.push(
+
+ );
+ });
+
+ panelRouterEl.push(
+
+
+
+ );
+
+ sidebarEl.push(
+
+
+
+
+
+
+
+
+ );
+ });
+
+ return { sidebar, panelRouter };
+};
\ No newline at end of file
diff --git a/web/source/panels/lib/panel.js b/web/source/settings-panel/lib/panel.js
similarity index 79%
rename from web/source/panels/lib/panel.js
rename to web/source/settings-panel/lib/panel.js
index 168eac7a0..df723bc74 100644
--- a/web/source/panels/lib/panel.js
+++ b/web/source/settings-panel/lib/panel.js
@@ -1,19 +1,19 @@
/*
- GoToSocial
- Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
*/
"use strict";
diff --git a/web/source/settings-panel/lib/submit.js b/web/source/settings-panel/lib/submit.js
new file mode 100644
index 000000000..f268b5cf9
--- /dev/null
+++ b/web/source/settings-panel/lib/submit.js
@@ -0,0 +1,48 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+
+module.exports = function submit(func, {
+ setStatus, setError,
+ startStatus="PATCHing", successStatus="Saved!",
+ onSuccess,
+ onError
+}) {
+ return function() {
+ setStatus(startStatus);
+ setError("");
+ return Promise.try(() => {
+ return func();
+ }).then(() => {
+ setStatus(successStatus);
+ if (onSuccess != undefined) {
+ return onSuccess();
+ }
+ }).catch((e) => {
+ setError(e.message);
+ setStatus("");
+ console.error(e);
+ if (onError != undefined) {
+ onError(e);
+ }
+ });
+ };
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/index.js b/web/source/settings-panel/redux/index.js
new file mode 100644
index 000000000..e0dbe9b23
--- /dev/null
+++ b/web/source/settings-panel/redux/index.js
@@ -0,0 +1,48 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const { createStore, combineReducers, applyMiddleware } = require("redux");
+const { persistStore, persistReducer } = require("redux-persist");
+const thunk = require("redux-thunk").default;
+const { composeWithDevTools } = require("redux-devtools-extension");
+
+const persistConfig = {
+ key: "gotosocial-settings",
+ storage: require("redux-persist/lib/storage").default,
+ stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel2").default,
+ whitelist: ["oauth"],
+ blacklist: ["temporary"]
+};
+
+const combinedReducers = combineReducers({
+ oauth: require("./reducers/oauth").reducer,
+ instances: require("./reducers/instances").reducer,
+ temporary: require("./reducers/temporary").reducer,
+ user: require("./reducers/user").reducer,
+ admin: require("./reducers/admin").reducer,
+});
+
+const persistedReducer = persistReducer(persistConfig, combinedReducers);
+const composedEnhancer = composeWithDevTools(applyMiddleware(thunk));
+
+const store = createStore(persistedReducer, composedEnhancer);
+const persistor = persistStore(store);
+
+module.exports = { store, persistor };
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/reducers/admin.js b/web/source/settings-panel/redux/reducers/admin.js
new file mode 100644
index 000000000..20d3d748d
--- /dev/null
+++ b/web/source/settings-panel/redux/reducers/admin.js
@@ -0,0 +1,131 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const { createSlice } = require("@reduxjs/toolkit");
+const defaultValue = require("default-value");
+
+function sortBlocks(blocks) {
+ return blocks.sort((a, b) => { // alphabetical sort
+ return a.domain.localeCompare(b.domain);
+ });
+}
+
+function emptyBlock() {
+ return {
+ public_comment: "",
+ private_comment: "",
+ obfuscate: false
+ };
+}
+
+function emptyEmojiForm() {
+ return {
+ shortcode: ""
+ };
+}
+
+module.exports = createSlice({
+ name: "admin",
+ initialState: {
+ loadedBlockedInstances: false,
+ blockedInstances: undefined,
+ bulkBlock: {
+ list: "",
+ exportType: "plain",
+ ...emptyBlock()
+ },
+ newInstanceBlocks: {},
+ emoji: {},
+ newEmoji: emptyEmojiForm()
+ },
+ reducers: {
+ setBlockedInstances: (state, { payload }) => {
+ state.blockedInstances = {};
+ sortBlocks(payload).forEach((entry) => {
+ state.blockedInstances[entry.domain] = entry;
+ });
+ state.loadedBlockedInstances = true;
+ },
+
+ newDomainBlock: (state, { payload: [domain, data] }) => {
+ if (data == undefined) {
+ data = {
+ new: true,
+ domain,
+ ...emptyBlock()
+ };
+ }
+ state.newInstanceBlocks[domain] = data;
+ },
+
+ setDomainBlock: (state, { payload: [domain, data = {}] }) => {
+ state.blockedInstances[domain] = data;
+ },
+
+ removeDomainBlock: (state, {payload: domain}) => {
+ delete state.blockedInstances[domain];
+ },
+
+ updateDomainBlockVal: (state, { payload: [domain, key, val] }) => {
+ state.newInstanceBlocks[domain][key] = val;
+ },
+
+ updateBulkBlockVal: (state, { payload: [key, val] }) => {
+ state.bulkBlock[key] = val;
+ },
+
+ resetBulkBlockVal: (state, { _payload }) => {
+ state.bulkBlock = {
+ list: "",
+ exportType: "plain",
+ ...emptyBlock()
+ };
+ },
+
+ exportToField: (state, { _payload }) => {
+ state.bulkBlock.list = Object.values(state.blockedInstances).map((entry) => {
+ return entry.domain;
+ }).join("\n");
+ },
+
+ setEmoji: (state, {payload}) => {
+ state.emoji = {};
+ payload.forEach((emoji) => {
+ if (emoji.category == undefined) {
+ emoji.category = "Unsorted";
+ }
+ state.emoji[emoji.category] = defaultValue(state.emoji[emoji.category], []);
+ state.emoji[emoji.category].push(emoji);
+ });
+ },
+
+ updateNewEmojiVal: (state, { payload: [key, val] }) => {
+ state.newEmoji[key] = val;
+ },
+
+ addEmoji: (state, {payload: emoji}) => {
+ if (emoji.category == undefined) {
+ emoji.category = "Unsorted";
+ }
+ state.emoji[emoji.category] = defaultValue(state.emoji[emoji.category], []);
+ state.emoji[emoji.category].push(emoji);
+ },
+ }
+});
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/reducers/instances.js b/web/source/settings-panel/redux/reducers/instances.js
new file mode 100644
index 000000000..3ad5bb7cb
--- /dev/null
+++ b/web/source/settings-panel/redux/reducers/instances.js
@@ -0,0 +1,42 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const {createSlice} = require("@reduxjs/toolkit");
+const d = require("dotty");
+
+module.exports = createSlice({
+ name: "instances",
+ initialState: {
+ info: {},
+ },
+ reducers: {
+ setNamedInstanceInfo: (state, {payload}) => {
+ let [key, info] = payload;
+ state.info[key] = info;
+ },
+ setInstanceInfo: (state, {payload}) => {
+ state.current = payload;
+ state.adminSettings = payload;
+ },
+ setAdminSettingsVal: (state, {payload: [key, val]}) => {
+ d.put(state.adminSettings, key, val);
+ }
+ }
+});
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/reducers/oauth.js b/web/source/settings-panel/redux/reducers/oauth.js
new file mode 100644
index 000000000..c332a7d06
--- /dev/null
+++ b/web/source/settings-panel/redux/reducers/oauth.js
@@ -0,0 +1,52 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const {createSlice} = require("@reduxjs/toolkit");
+
+module.exports = createSlice({
+ name: "oauth",
+ initialState: {
+ loginState: 'none',
+ },
+ reducers: {
+ setInstance: (state, {payload}) => {
+ state.instance = payload;
+ },
+ setRegistration: (state, {payload}) => {
+ state.registration = payload;
+ },
+ setLoginState: (state, {payload}) => {
+ state.loginState = payload;
+ },
+ login: (state, {payload}) => {
+ state.token = `${payload.token_type} ${payload.access_token}`;
+ state.loginState = "login";
+ },
+ remove: (state, {_payload}) => {
+ delete state.token;
+ delete state.registration;
+ delete state.isAdmin;
+ state.loginState = "none";
+ },
+ setAdmin: (state, {payload}) => {
+ state.isAdmin = payload;
+ }
+ }
+});
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/reducers/temporary.js b/web/source/settings-panel/redux/reducers/temporary.js
new file mode 100644
index 000000000..c887d2eee
--- /dev/null
+++ b/web/source/settings-panel/redux/reducers/temporary.js
@@ -0,0 +1,32 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const {createSlice} = require("@reduxjs/toolkit");
+
+module.exports = createSlice({
+ name: "temporary",
+ initialState: {
+ },
+ reducers: {
+ setStatus: function(state, {payload}) {
+ state.status = payload;
+ }
+ }
+});
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/reducers/user.js b/web/source/settings-panel/redux/reducers/user.js
new file mode 100644
index 000000000..b4463c9f9
--- /dev/null
+++ b/web/source/settings-panel/redux/reducers/user.js
@@ -0,0 +1,51 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const { createSlice } = require("@reduxjs/toolkit");
+const d = require("dotty");
+const defaultValue = require("default-value");
+
+module.exports = createSlice({
+ name: "user",
+ initialState: {
+ profile: {},
+ settings: {}
+ },
+ reducers: {
+ setAccount: (state, { payload }) => {
+ payload.source = defaultValue(payload.source, {});
+ payload.source.language = defaultValue(payload.source.language.toUpperCase(), "EN");
+ payload.source.status_format = defaultValue(payload.source.status_format, "plain");
+ payload.source.sensitive = defaultValue(payload.source.sensitive, false);
+
+ state.profile = payload;
+ // /user/settings only needs a copy of the 'source' obj
+ state.settings = {
+ source: payload.source
+ };
+ },
+ setProfileVal: (state, { payload: [key, val] }) => {
+ d.put(state.profile, key, val);
+ },
+ setSettingsVal: (state, { payload: [key, val] }) => {
+ d.put(state.settings, key, val);
+ }
+ }
+});
\ No newline at end of file
diff --git a/web/source/settings-panel/style.css b/web/source/settings-panel/style.css
new file mode 100644
index 000000000..35d11fa08
--- /dev/null
+++ b/web/source/settings-panel/style.css
@@ -0,0 +1,498 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+body {
+ grid-template-rows: auto 1fr;
+}
+
+.content {
+ grid-column: 1 / span 3; /* stretch entire width, to fit panel + sidebar nav */
+}
+
+section {
+ grid-column: 2;
+}
+
+#root {
+ display: grid;
+ grid-template-columns: 1fr 90ch 1fr;
+ width: 100vw;
+ max-width: 100vw;
+ box-sizing: border-box;
+
+ section.with-sidebar {
+ border-left: none;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ & > div {
+ border-left: 0.2rem solid $border-accent;
+ padding-left: 0.4rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ margin: 2rem 0;
+
+ h2 {
+ margin: 0;
+ margin-bottom: 0.5rem;
+ }
+
+ &:only-child {
+ border-left: none;
+ }
+
+ &:first-child {
+ margin-top: 0;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .sidebar {
+ align-self: start;
+ justify-self: end;
+ background: $settings-nav-bg;
+ border: $boxshadow-border;
+ box-shadow: $boxshadow;
+ border-radius: $br;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ display: flex;
+ flex-direction: column;
+ min-width: 12rem;
+
+ a {
+ text-decoration: none;
+ }
+
+ a:first-child h2 {
+ border-top-left-radius: $br;
+ }
+
+ h2 {
+ margin: 0;
+ padding: 0.5rem;
+ font-size: 0.9rem;
+ font-weight: bold;
+ text-transform: uppercase;
+ color: $settings-nav-header-fg;
+ background: $settings-nav-header-bg;
+ }
+
+ nav {
+ display: flex;
+ flex-direction: column;
+
+ a {
+ padding: 1rem;
+ text-decoration: none;
+ transition: 0.1s;
+ color: $fg;
+
+ &:hover {
+ color: $settings-nav-fg-hover;
+ background: $settings-nav-bg-hover;
+ }
+
+ &.active {
+ color: $settings-nav-fg-active;
+ background: $settings-nav-bg-active;
+ font-weight: bold;
+ text-decoration: underline;
+ }
+
+ /* reserve space for bold version of the element, so .active doesn't
+ change container size */
+ &::after {
+ font-weight: bold;
+ text-decoration: underline;
+ display: block;
+ content: attr(data-content);
+ height: 1px;
+ color: transparent;
+ overflow: hidden;
+ visibility: hidden;
+ }
+ }
+ }
+
+
+ nav:last-child a:last-child {
+ border-bottom-left-radius: $br;
+ border-bottom: none;
+ }
+ }
+}
+
+.capitalize {
+ text-transform: capitalize;
+}
+
+section {
+ margin-bottom: 1rem;
+}
+
+input, select, textarea {
+ box-sizing: border-box;
+}
+
+.error {
+ color: $error-fg;
+ background: $error-bg;
+ border: 0.02rem solid $error-fg;
+ border-radius: $br;
+ font-weight: bold;
+ padding: 0.5rem;
+ white-space: pre-wrap;
+
+ a {
+ color: $error-link;
+ }
+
+ pre {
+ background: $bg;
+ color: $fg;
+ padding: 1rem;
+ overflow: auto;
+ margin: 0;
+ }
+}
+
+.hidden {
+ display: none;
+}
+
+.messagebutton, .messagebutton > div {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+
+ div.padded {
+ margin-left: 1rem;
+ }
+
+ button, .button {
+ white-space: nowrap;
+ margin-right: 1rem;
+ }
+}
+
+.messagebutton > div {
+ button, .button {
+ margin-top: 1rem;
+ }
+}
+
+.notImplemented {
+ border: 2px solid rgb(70, 79, 88);
+ background: repeating-linear-gradient(
+ -45deg,
+ #525c66,
+ #525c66 10px,
+ rgb(70, 79, 88) 10px,
+ rgb(70, 79, 88) 20px
+ ) !important;
+}
+
+section.with-sidebar > div {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ input, textarea {
+ width: 100%;
+ line-height: 1.5rem;
+ }
+
+ input[type=checkbox] {
+ justify-self: start;
+ width: initial;
+ }
+
+ input:read-only {
+ border: none;
+ }
+
+ input:invalid {
+ border-color: red;
+ }
+
+ textarea {
+ width: 100%;
+ }
+
+ h1 {
+ margin-bottom: 0.5rem;
+ }
+
+ .moreinfolink {
+ font-size: 0.9em;
+ }
+
+ .labelinput .border {
+ border-radius: 0.2rem;
+ border: 0.15rem solid $border_accent;
+ padding: 0.3rem;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .file-input.button {
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: normal;
+ padding: 0.3rem 0.3rem;
+ align-self: flex-start;
+ margin-right: 0.2rem;
+ }
+
+ .labelinput, .labelselect {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ }
+
+ .labelcheckbox {
+ display: flex;
+ gap: 0.4rem;
+ }
+
+ .titlesave {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+ }
+}
+
+.file-upload > div {
+ display: flex;
+ gap: 1rem;
+
+ img {
+ height: 8rem;
+ border: 0.2rem solid $border-accent;
+ }
+
+ img.avatar {
+ width: 8rem;
+ }
+
+ img.header {
+ width: 24rem;
+ }
+}
+
+.user-profile {
+ .overview {
+ display: grid;
+ grid-template-columns: 70% 30%;
+
+ .basic {
+ margin-top: -4.5rem;
+
+ .avatar {
+ height: 5rem;
+ width: 5rem;
+ }
+
+ .displayname {
+ font-size: 1.3rem;
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-top: 0.7rem;
+ }
+ }
+
+ .files {
+ width: 100%;
+ margin: 1rem;
+ margin-right: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ div.form-field {
+ width: 100%;
+ display: flex;
+
+ span {
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding: 0.3rem 0;
+ }
+ }
+
+ h3 {
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+ }
+
+ div:first-child {
+ margin-bottom: 1rem;
+ }
+
+ span {
+ font-style: italic;
+ }
+ }
+ }
+}
+
+.form-field label {
+ font-weight: bold;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ margin-top: 0.5rem;
+ max-height: 40rem;
+ overflow: auto;
+
+ .entry {
+ display: flex;
+ flex-wrap: wrap;
+ background: $settings-entry-bg;
+
+ &:hover {
+ background: $settings-entry-hover-bg;
+ }
+ }
+}
+
+.instance-list {
+ .filter {
+ display: flex;
+ gap: 0.5rem;
+
+ input {
+ width: auto;
+ flex: 1 1 auto;
+ }
+ }
+
+ .entry {
+ padding: 0.3rem;
+ margin: 0.2rem 0;
+
+ #domain {
+ flex: 1 1 auto;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+}
+
+.bulk h2 {
+ display: flex;
+ justify-content: space-between;
+}
+
+.emoji-list {
+ background: $settings-entry-bg;
+
+ .entry {
+ padding: 0.5rem;
+ flex-direction: column;
+
+ .emoji-group {
+ display: flex;
+
+ a {
+ border-radius: $br;
+ padding: 0.4rem;
+ line-height: 0;
+
+ img {
+ height: 2rem;
+ width: 2rem;
+ object-fit: contain;
+ vertical-align: middle;
+ }
+
+ &:hover {
+ background: $settings-entry-hover-bg;
+ }
+ }
+ }
+
+ &:hover {
+ background: inherit;
+ }
+ }
+}
+
+.toot {
+ padding-top: 0.5rem;
+ .contentgrid {
+ padding: 0 0.5rem;
+ }
+}
+
+@media screen and (max-width: 100ch) {
+ #root {
+ padding: 1rem;
+ grid-template-columns: 100%;
+ grid-template-rows: auto auto;
+
+ .sidebar {
+ justify-self: auto;
+ margin-bottom: 2rem;
+ }
+
+ .sidebar, section.with-sidebar {
+ border-top-left-radius: $br;
+ border-top-right-radius: $br;
+ border-bottom-left-radius: $br;
+ border-bottom-right-radius: $br;
+ }
+
+ .sidebar a:first-child h2 {
+ border-top-right-radius: $br;
+ }
+ }
+
+ section {
+ grid-column: 1;
+ }
+
+ .user-profile .overview {
+ grid-template-columns: 100%;
+ grid-template-rows: auto auto;
+
+ .files {
+ margin: 0;
+ margin-top: 1rem;
+ }
+ }
+
+ main section {
+ padding: 0.75rem;
+ }
+
+ .instance-list .filter {
+ flex-direction: column;
+ }
+}
\ No newline at end of file
diff --git a/web/source/settings-panel/user/profile.js b/web/source/settings-panel/user/profile.js
new file mode 100644
index 000000000..7cf3a7b52
--- /dev/null
+++ b/web/source/settings-panel/user/profile.js
@@ -0,0 +1,113 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+
+const Submit = require("../components/submit");
+
+const api = require("../lib/api");
+const user = require("../redux/reducers/user").actions;
+const submit = require("../lib/submit");
+
+const { formFields } = require("../components/form-fields");
+
+const {
+ TextInput,
+ TextArea,
+ Checkbox,
+ File
+} = formFields(user.setProfileVal, (state) => state.user.profile);
+
+module.exports = function UserProfile() {
+ const dispatch = Redux.useDispatch();
+ const account = Redux.useSelector(state => state.user.profile);
+ const instance = Redux.useSelector(state => state.instances.current);
+
+ const allowCustomCSS = instance.configuration.accounts.allow_custom_css;
+
+ const [errorMsg, setError] = React.useState("");
+ const [statusMsg, setStatus] = React.useState("");
+
+ const saveProfile = submit(
+ () => dispatch(api.user.updateProfile()),
+ {setStatus, setError}
+ );
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/user/settings.js b/web/source/settings-panel/user/settings.js
new file mode 100644
index 000000000..ccb3e911d
--- /dev/null
+++ b/web/source/settings-panel/user/settings.js
@@ -0,0 +1,140 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+
+const api = require("../lib/api");
+const user = require("../redux/reducers/user").actions;
+const submit = require("../lib/submit");
+
+const Languages = require("../components/languages");
+const Submit = require("../components/submit");
+
+const {
+ Checkbox,
+ Select,
+} = require("../components/form-fields").formFields(user.setSettingsVal, (state) => state.user.settings);
+
+module.exports = function UserSettings() {
+ const dispatch = Redux.useDispatch();
+
+ const [errorMsg, setError] = React.useState("");
+ const [statusMsg, setStatus] = React.useState("");
+
+ const updateSettings = submit(
+ () => dispatch(api.user.updateSettings()),
+ {setStatus, setError}
+ );
+
+ return (
+ <>
+