mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-22 18:22:11 +00:00
[frontend] Unified panels (#812)
* settings panel restructuring * clean up old Gin handlers * colorscheme redesign, some other small css tweaks * basic router layout, error boundary * colorscheme redesign, some other small css tweaks * kebab-case consistency * superfluous padding on applist * remove unused consts * redux, whitespace changes.. * use .jsx extensions for components * login flow up till app registration * full redux oauth implementation, with basic error handling * split oauth api functions * oauth api revocation handling * basic profile change submission * move old dir * profile overview * fix keeping track of the wrong instance url (for different instance/api domains) * use redux state for profile form * delete old/index.js, old/basic.js, fully implemented * implement old/user/profile.js * implement password change * remove debug logging * support future api for removing files * customize profile css * remove unneeded wrapper components * restructure form fields * start on admin pages * admin panel settings * admin settings panel * remove old/admin files * add top-level redirect * refactor/cleanup forms * only do API checks on logged-in state * admin-status based routing * federation block routing * federation blocks * upgrade dependencies * react 18 changes * media cleanup * fix useEffect hooks * remove unused require * custom emoji base * emoji uploader * delete last old panel files * sidebar styling, remove unused page * refactor submit functions * fix sidebar boxshadow-border * fix old css variables * fix fake-toot avatar * fix non-square emoji * fix user settings redux keys * properly get admin account contact from instance response * Account.source default values * source.status_format key * mobile responsiveness * mobile element tweaks * proper redirect after removing block * add redirects for old setting panel urls * deletes * fix mobile overflow * clean up debug logging calls
This commit is contained in:
parent
2f22780800
commit
938328cd07
|
@ -117,6 +117,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
||||||
"show_back_to_top": showBackToTop,
|
"show_back_to_top": showBackToTop,
|
||||||
"stylesheets": stylesheets,
|
"stylesheets": stylesheets,
|
||||||
"javascript": []string{
|
"javascript": []string{
|
||||||
|
"/assets/dist/bundle.js",
|
||||||
"/assets/dist/frontend.js",
|
"/assets/dist/frontend.js",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) UserPanelHandler(c *gin.Context) {
|
func (m *Module) SettingsPanelHandler(c *gin.Context) {
|
||||||
host := config.GetHost()
|
host := config.GetHost()
|
||||||
instance, err := m.processor.InstanceGet(c.Request.Context(), host)
|
instance, err := m.processor.InstanceGet(c.Request.Context(), host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -41,37 +41,13 @@ func (m *Module) UserPanelHandler(c *gin.Context) {
|
||||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
||||||
assetsPathPrefix + "/dist/_colors.css",
|
assetsPathPrefix + "/dist/_colors.css",
|
||||||
assetsPathPrefix + "/dist/base.css",
|
assetsPathPrefix + "/dist/base.css",
|
||||||
assetsPathPrefix + "/dist/panels-base.css",
|
assetsPathPrefix + "/dist/profile.css",
|
||||||
assetsPathPrefix + "/dist/panels-user-style.css",
|
assetsPathPrefix + "/dist/status.css",
|
||||||
|
assetsPathPrefix + "/dist/settings-panel-style.css",
|
||||||
},
|
},
|
||||||
"javascript": []string{
|
"javascript": []string{
|
||||||
assetsPathPrefix + "/dist/bundle.js",
|
assetsPathPrefix + "/dist/bundle.js",
|
||||||
assetsPathPrefix + "/dist/user-panel.js",
|
assetsPathPrefix + "/dist/settings.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",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -119,6 +119,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
|
||||||
"ogMeta": ogBase(instance).withStatus(status),
|
"ogMeta": ogBase(instance).withStatus(status),
|
||||||
"stylesheets": stylesheets,
|
"stylesheets": stylesheets,
|
||||||
"javascript": []string{
|
"javascript": []string{
|
||||||
|
"/assets/dist/bundle.js",
|
||||||
"/assets/dist/frontend.js",
|
"/assets/dist/frontend.js",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -37,9 +37,9 @@
|
||||||
profilePath = "/@:" + usernameKey
|
profilePath = "/@:" + usernameKey
|
||||||
customCSSPath = profilePath + "/custom.css"
|
customCSSPath = profilePath + "/custom.css"
|
||||||
statusPath = profilePath + "/statuses/:" + statusIDKey
|
statusPath = profilePath + "/statuses/:" + statusIDKey
|
||||||
adminPanelPath = "/admin"
|
|
||||||
userPanelpath = "/user"
|
|
||||||
assetsPathPrefix = "/assets"
|
assetsPathPrefix = "/assets"
|
||||||
|
userPanelPath = "/settings/user"
|
||||||
|
adminPanelPath = "/settings/admin"
|
||||||
|
|
||||||
tokenParam = "token"
|
tokenParam = "token"
|
||||||
usernameKey = "username"
|
usernameKey = "username"
|
||||||
|
@ -70,20 +70,24 @@ func (m *Module) Route(s router.Router) error {
|
||||||
assetsGroup := s.AttachGroup(assetsPathPrefix)
|
assetsGroup := s.AttachGroup(assetsPathPrefix)
|
||||||
m.mountAssetsFilesystem(assetsGroup)
|
m.mountAssetsFilesystem(assetsGroup)
|
||||||
|
|
||||||
s.AttachHandler(http.MethodGet, adminPanelPath, m.AdminPanelHandler)
|
s.AttachHandler(http.MethodGet, "/settings", m.SettingsPanelHandler)
|
||||||
// redirect /admin/ to /admin
|
s.AttachHandler(http.MethodGet, "/settings/*panel", m.SettingsPanelHandler)
|
||||||
s.AttachHandler(http.MethodGet, adminPanelPath+"/", func(c *gin.Context) {
|
|
||||||
c.Redirect(http.StatusMovedPermanently, adminPanelPath)
|
// 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)
|
// old version of settings panel
|
||||||
// redirect /user/ to /user
|
s.AttachHandler(http.MethodGet, "/user", func(c *gin.Context) {
|
||||||
s.AttachHandler(http.MethodGet, userPanelpath+"/", func(c *gin.Context) {
|
c.Redirect(http.StatusMovedPermanently, userPanelPath)
|
||||||
c.Redirect(http.StatusMovedPermanently, userPanelpath)
|
|
||||||
})
|
})
|
||||||
// redirect /auth/edit to /user
|
|
||||||
s.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) {
|
// Admin panel redirects
|
||||||
c.Redirect(http.StatusMovedPermanently, userPanelpath)
|
// old version of settings panel
|
||||||
|
s.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) {
|
||||||
|
c.Redirect(http.StatusMovedPermanently, adminPanelPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
// serve front-page
|
// serve front-page
|
||||||
|
|
|
@ -23,57 +23,85 @@
|
||||||
|
|
||||||
/* Color definitions */
|
/* 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;
|
/* Background shades, contrast >= 5.0 with $white1 (#fafaff) */
|
||||||
$sloth_gray2: #4d4e56;
|
$gray1: #2a2b2f;
|
||||||
|
$gray2: #35363b;
|
||||||
|
$gray3: #3a3b41;
|
||||||
|
$gray4: #45464e;
|
||||||
|
$gray5: #4d4e56;
|
||||||
|
$gray6: #575861;
|
||||||
|
$gray7: #5d5e67;
|
||||||
|
$gray8: #696a75;
|
||||||
|
|
||||||
$sloth_orange1: #e78e5a;
|
$orange1: #fd6a00; /* Used for non-text accent colors, can be used as background: $gray1 for text color (contrast 4.6)*/
|
||||||
$sloth_orange2: #D87841;
|
$orange2: #ff853e; /* hover/selected accent to $orange1, can be used with $gray1 (5.7), $gray2 (4.6) */
|
||||||
$blue: #63b1de; // complementary color to $sloth_orange1
|
|
||||||
|
|
||||||
/* 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%));
|
$error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */
|
||||||
$sloth_gray2_darker5: color-mod($sloth_gray2 lightness(-5%));
|
$error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */
|
||||||
$sloth_gray2_darker7: color-mod($sloth_gray2 lightness(-7%));
|
$error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */
|
||||||
$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%));
|
|
||||||
|
|
||||||
$blue_lighter8: color-mod($blue lightness(+4%));
|
$fg: $white1;
|
||||||
$lightblue: color-mod($blue lightness(+16%));
|
$bg: $gray1;
|
||||||
|
|
||||||
$fg: $near_white;
|
$bg-trans: color-mod($gray5 alpha(62%));
|
||||||
$bg: $sloth_gray2_darker7;
|
|
||||||
|
|
||||||
$bg_trans: color-mod($sloth_gray2 alpha(62%));
|
$bg-accent: $gray5;
|
||||||
|
$fg-accent: $blue3;
|
||||||
$bg_accent: $sloth_gray2_lighter3;
|
$fg-reduced: $white2;
|
||||||
$fg_accent: $lightblue;
|
$border-accent: $orange2;
|
||||||
$border_accent: $sloth_orange2;
|
|
||||||
|
|
||||||
/* Color variables as used in a specific location */
|
/* 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-danger-bg: $orange1;
|
||||||
$button_bg: $blue_lighter8;
|
$button-danger-fg: $gray1;
|
||||||
$button_fg: $sloth_gray2_darker15;
|
$button-danger-hover-bg: $orange2;
|
||||||
$button_hover_bg: $lightblue;
|
|
||||||
|
|
||||||
$status_focus_bg: $bg_accent;
|
$toot-focus-bg: $gray5;
|
||||||
$status_unfocus_bg: $sloth_gray2_darker3;
|
$toot-unfocus-bg: $gray3;
|
||||||
$status_info_fg: #CBCBD7;
|
|
||||||
|
|
||||||
$bg_no_img_desc: $sloth_orange2;
|
$toot-info-bg: $gray4;
|
||||||
$bg_sensitive: $sloth_gray2_darker15;
|
|
||||||
|
$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: 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;
|
$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;
|
|
@ -34,7 +34,7 @@
|
||||||
$br: 0.4rem;
|
$br: 0.4rem;
|
||||||
// border radius for items that are framed/bordered
|
// border radius for items that are framed/bordered
|
||||||
// inside something with $br, eg avatar, header img
|
// inside something with $br, eg avatar, header img
|
||||||
$br_inner: 0.2rem;
|
$br-inner: 0.2rem;
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -42,7 +42,7 @@ html, body {
|
||||||
background: $bg;
|
background: $bg;
|
||||||
color: $fg;
|
color: $fg;
|
||||||
font-family: "Noto Sans", sans-serif;
|
font-family: "Noto Sans", sans-serif;
|
||||||
scrollbar-color: $sloth_orange1 $sloth_gray2_darker3;
|
scrollbar-color: $orange1 $gray3;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -71,7 +71,7 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $link_fg;
|
color: $link-fg;
|
||||||
}
|
}
|
||||||
|
|
||||||
header, footer {
|
header, footer {
|
||||||
|
@ -83,9 +83,13 @@ header, footer {
|
||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
header a {
|
header a {
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
/* background: $header_bg; */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -109,7 +113,7 @@ header a {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.excerpt_top {
|
.excerpt-top {
|
||||||
margin-top: -1rem;
|
margin-top: -1rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
@ -119,15 +123,15 @@ header a {
|
||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: $fg_accent;
|
color: $fg-accent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
section {
|
section {
|
||||||
background: $bg_accent;
|
background: $bg-accent;
|
||||||
box-shadow: $boxshadow;
|
box-shadow: $boxshadow;
|
||||||
border: $boxshadow_border;
|
border: $boxshadow-border;
|
||||||
border-radius: $br;
|
border-radius: $br;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
@ -144,10 +148,10 @@ main {
|
||||||
|
|
||||||
.button, button {
|
.button, button {
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
color: $button_fg;
|
color: $button-fg;
|
||||||
background: $button_bg;
|
background: $button-bg;
|
||||||
box-shadow: $boxshadow;
|
box-shadow: $boxshadow;
|
||||||
border: $button_border;
|
border: $button-border;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -157,8 +161,17 @@ main {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: 'Noto Sans', sans-serif;
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
color: $button-danger-fg;
|
||||||
|
background: $button-danger-bg;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $button_hover_bg;
|
background: $button-danger-hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $button-hover-bg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +204,7 @@ section.apps {
|
||||||
grid-template-columns: 25% 1fr;
|
grid-template-columns: 25% 1fr;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
background: $bg_accent;
|
background: $bg-accent;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
@ -211,7 +224,7 @@ section.apps {
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
padding: 1rem 0;
|
padding: 0;
|
||||||
h3 {
|
h3 {
|
||||||
margin-top: 0;
|
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 {
|
input, select, textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 0.15rem solid $border_accent;
|
border: 0.15rem solid $input-border;
|
||||||
border-radius: 0.1rem;
|
border-radius: 0.1rem;
|
||||||
color: $fg;
|
color: $fg;
|
||||||
/* background: $input_bg; */
|
background: $input-bg;
|
||||||
background: $bg_accent;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: 'Noto Sans', sans-serif;
|
font-family: 'Noto Sans', sans-serif;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 0.3rem;
|
padding: 0.3rem;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: $fg_accent;
|
border-color: $input-focus-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: $input-disabled-bg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea {
|
::placeholder {
|
||||||
padding-top: 0.1rem;
|
opacity: 1;
|
||||||
padding-bottom: 0.1rem;
|
color: $fg-reduced
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 0.02rem solid $border-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
@ -331,3 +360,7 @@ footer {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile {
|
.profile {
|
||||||
background: $bg_accent;
|
background: $bg-accent;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto auto;
|
grid-template-rows: auto auto auto;
|
||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
|
@ -38,7 +38,7 @@ main {
|
||||||
|
|
||||||
border-radius: $br;
|
border-radius: $br;
|
||||||
box-shadow: $boxshadow;
|
box-shadow: $boxshadow;
|
||||||
border: $boxshadow_border;
|
border: $boxshadow-border;
|
||||||
|
|
||||||
.headerimage {
|
.headerimage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -50,7 +50,7 @@ main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
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 {
|
#profile-basic-filler2 {
|
||||||
grid-area: filler2;
|
grid-area: filler2;
|
||||||
background: $bg_trans;
|
background: $bg-trans;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
|
@ -79,7 +79,7 @@ main {
|
||||||
width: 8.5rem;
|
width: 8.5rem;
|
||||||
grid-area: avatar;
|
grid-area: avatar;
|
||||||
background: $bg;
|
background: $bg;
|
||||||
border: $profile_avatar_border;
|
border: 0.2rem solid $avatar-border;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: $br;
|
border-radius: $br;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -87,7 +87,7 @@ main {
|
||||||
box-shadow: $boxshadow;
|
box-shadow: $boxshadow;
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: $br_inner;
|
border-radius: $br-inner;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ main {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
line-height: 2.2rem;
|
line-height: 2.2rem;
|
||||||
background: $bg_trans;
|
background: $bg-trans;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -120,7 +120,7 @@ main {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
color: $fg_accent;
|
color: $fg-accent;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
|
@ -31,13 +31,13 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
.toot {
|
.toot {
|
||||||
background: $status_unfocus_bg;
|
background: $toot-unfocus-bg;
|
||||||
box-shadow: $boxshadow;
|
box-shadow: $boxshadow;
|
||||||
border: $boxshadow_border;
|
border: $boxshadow-border;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: $br;
|
margin-bottom: $br;
|
||||||
border-radius: $br;
|
padding-top: 1.5rem;
|
||||||
padding: 1.5rem 0;
|
padding-bottom: 0.7rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -49,27 +49,34 @@ main {
|
||||||
.contentgrid {
|
.contentgrid {
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 4rem auto 1fr;
|
grid-template-columns: 4rem 1fr auto;
|
||||||
grid-template-rows: 1.5rem auto auto;
|
grid-template-rows: 1.5rem auto auto auto;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.not-expanded {
|
||||||
|
color: $fg-reduced;
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
grid-row: span 2;
|
grid-row: span 3;
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
|
display: flex;
|
||||||
|
border: 0.2rem solid $avatar-border;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
overflow: hidden; /* hides corners from img overflowing */
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
background: $bg;
|
background: $bg;
|
||||||
border: 0.1rem solid $acc2;
|
|
||||||
border-radius: calc($br / 1.5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.displayname {
|
.displayname {
|
||||||
grid-column: span 2;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
@ -82,7 +89,7 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
color: $link_fg;
|
color: $link-fg;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
margin-top: -0.5rem;
|
margin-top: -0.5rem;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
|
@ -119,8 +126,7 @@ main {
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 0.5rem;
|
grid-column: 2 / span 2;
|
||||||
grid-column: span 3;
|
|
||||||
grid-row: span 1;
|
grid-row: span 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@ -128,34 +134,33 @@ main {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $link_fg;
|
color: $link-fg;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding-top: 0.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
padding: 0.5rem 0 0.5rem 1.5rem;
|
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;
|
margin-left: 1rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: 1px dashed $sloth_orange1;
|
border: 1px dashed $border-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre, code {
|
pre, code {
|
||||||
background-color: $sloth_gray2_darker7;
|
background-color: $gray2;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
border-radius: $br_inner;
|
border-radius: $br-inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
@ -249,7 +254,7 @@ main {
|
||||||
|
|
||||||
.closed {
|
.closed {
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
background: $bg_sensitive;
|
background: $bg-sensitive;
|
||||||
@supports (backdrop-filter: blur(2rem)) {
|
@supports (backdrop-filter: blur(2rem)) {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
backdrop-filter: blur(2rem);
|
backdrop-filter: blur(2rem);
|
||||||
|
@ -263,17 +268,17 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-image-desc {
|
.no-image-desc {
|
||||||
color: $button_fg;
|
color: $no-img-desc-fg;
|
||||||
|
background: $no-img-desc-bg;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.1rem;
|
bottom: 0.1rem;
|
||||||
right: 0.4rem;
|
right: 0.4rem;
|
||||||
margin-bottom: 0.4rem;
|
margin-bottom: 0.4rem;
|
||||||
margin-right: 0.4rem;
|
margin-right: 0.4rem;
|
||||||
background: $bg_no_img_desc;
|
|
||||||
padding: 0.1rem 0.45rem;
|
padding: 0.1rem 0.45rem;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
border: 0.2rem solid $button_fg;
|
border: 0.2rem solid $button-fg;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
|
||||||
i.fa {
|
i.fa {
|
||||||
|
@ -302,12 +307,13 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
background: $toot-info-bg;
|
||||||
|
color: $fg-reduced;
|
||||||
display: none;
|
display: none;
|
||||||
border-top: 0.15rem solid $status_unfocus_bg;
|
border-top: 0.15rem solid $toot-info-border;
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 0.5rem 1.5rem;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
position: relative;
|
|
||||||
padding-right: 1.3rem;
|
padding-right: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,30 +323,6 @@ main {
|
||||||
|
|
||||||
grid-column: span 3;
|
grid-column: span 3;
|
||||||
flex-wrap: wrap;
|
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 {
|
.toot-link {
|
||||||
|
@ -362,7 +344,7 @@ main {
|
||||||
border-top-right-radius: $br;
|
border-top-right-radius: $br;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child, &:last-child .info {
|
||||||
/* bottom left, bottom right */
|
/* bottom left, bottom right */
|
||||||
border-bottom-left-radius: $br;
|
border-bottom-left-radius: $br;
|
||||||
border-bottom-right-radius: $br;
|
border-bottom-right-radius: $br;
|
||||||
|
@ -370,11 +352,21 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
background: $status_focus_bg;
|
background: $toot-focus-bg;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|
||||||
.contentgrid {
|
.contentgrid {
|
||||||
padding-bottom: 1rem;
|
.displayname {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
grid-column: 1 / span 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-expanded {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
|
|
@ -18,11 +18,6 @@
|
||||||
|
|
||||||
"use strict";
|
"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 Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js");
|
||||||
const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js");
|
const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js");
|
||||||
const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default;
|
const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default;
|
||||||
|
|
|
@ -23,7 +23,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const path = require('path');
|
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 babelify = require('babelify');
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
@ -38,8 +39,9 @@ const splitCSS = require("./lib/split-css.js");
|
||||||
|
|
||||||
const bundles = {
|
const bundles = {
|
||||||
"./frontend/index.js": "frontend.js",
|
"./frontend/index.js": "frontend.js",
|
||||||
"./panels/admin/index.js": "admin-panel.js",
|
"./settings-panel/index.js": "settings.js",
|
||||||
"./panels/user/index.js": "user-panel.js",
|
// "./panels/admin/index.js": "admin-panel.js",
|
||||||
|
// "./panels/user/index.js": "user-panel.js",
|
||||||
};
|
};
|
||||||
|
|
||||||
const postcssPlugins = [
|
const postcssPlugins = [
|
||||||
|
@ -50,6 +52,18 @@ const postcssPlugins = [
|
||||||
"postcss-color-mod-function"
|
"postcss-color-mod-function"
|
||||||
].map((plugin) => require(plugin)());
|
].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 = {
|
const browserifyConfig = {
|
||||||
transform: [
|
transform: [
|
||||||
[
|
[
|
||||||
|
@ -69,10 +83,7 @@ const browserifyConfig = {
|
||||||
exclude: /node_modules\/(?!photoswipe-dynamic-caption-plugin)/,
|
exclude: /node_modules\/(?!photoswipe-dynamic-caption-plugin)/,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[require("uglifyify"), {
|
uglifyifyInProduction
|
||||||
global: true,
|
|
||||||
exts: ".js"
|
|
||||||
}]
|
|
||||||
],
|
],
|
||||||
plugin: [
|
plugin: [
|
||||||
[require("icssify"), {
|
[require("icssify"), {
|
||||||
|
@ -86,7 +97,8 @@ const browserifyConfig = {
|
||||||
return out(file);
|
return out(file);
|
||||||
})
|
})
|
||||||
}]
|
}]
|
||||||
]
|
],
|
||||||
|
extensions: [".js", ".jsx", ".css"]
|
||||||
};
|
};
|
||||||
|
|
||||||
const entryFiles = Object.keys(bundles);
|
const entryFiles = Object.keys(bundles);
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const React = require("react");
|
|
||||||
|
|
||||||
module.exports = function Submit({onClick, label, errorMsg, statusMsg}) {
|
|
||||||
return (
|
|
||||||
<div className="messagebutton">
|
|
||||||
<button type="submit" onClick={onClick}>{ label }</button>
|
|
||||||
<div className="error accent">{errorMsg ? errorMsg : statusMsg}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "gotosocial-frontend",
|
"name": "gotosocial-frontend",
|
||||||
"version": "0.3.8",
|
"version": "0.5.0",
|
||||||
"description": "GoToSocial frontend sources",
|
"description": "GoToSocial frontend sources",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "f0x",
|
"author": "f0x",
|
||||||
|
@ -9,18 +9,23 @@
|
||||||
"@babel/core": "^7.12.13",
|
"@babel/core": "^7.12.13",
|
||||||
"@babel/preset-env": "^7.12.13",
|
"@babel/preset-env": "^7.12.13",
|
||||||
"@babel/preset-react": "^7.12.13",
|
"@babel/preset-react": "^7.12.13",
|
||||||
|
"@f0x52/budo-express": "^1.1.0",
|
||||||
|
"@reduxjs/toolkit": "^1.8.5",
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"babelify": "^10.0.0",
|
"babelify": "^10.0.0",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"browserlist": "^1.0.1",
|
"browserlist": "^1.0.1",
|
||||||
"budo-express": "^1.0.8",
|
"create-error": "^0.3.1",
|
||||||
"css-extract": "^2.0.0",
|
"css-extract": "^2.0.0",
|
||||||
|
"default-value": "^1.0.0",
|
||||||
|
"dotty": "^0.1.2",
|
||||||
"eslint-plugin-react": "^7.24.0",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"factor-bundle": "^2.5.0",
|
"factor-bundle": "^2.5.0",
|
||||||
"from2-string": "^1.1.0",
|
|
||||||
"icssify": "^2.0.0",
|
"icssify": "^2.0.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"is-valid-domain": "^0.1.6",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"modern-normalize": "^1.1.0",
|
"modern-normalize": "^1.1.0",
|
||||||
"photoswipe": "^5.3.0",
|
"photoswipe": "^5.3.0",
|
||||||
|
@ -31,11 +36,17 @@
|
||||||
"postcss-nested": "^5.0.6",
|
"postcss-nested": "^5.0.6",
|
||||||
"postcss-scss": "^4.0.4",
|
"postcss-scss": "^4.0.4",
|
||||||
"postcss-strip-inline-comments": "^0.1.5",
|
"postcss-strip-inline-comments": "^0.1.5",
|
||||||
"pretty-bytes": "^5.6.0",
|
"prettier-bytes": "^1.0.4",
|
||||||
"react": "^17.0.1",
|
"pretty-bytes": "4",
|
||||||
"react-dom": "^17.0.1",
|
"react": "18",
|
||||||
"reactify": "^1.1.1",
|
"react-dom": "18",
|
||||||
"uglifyify": "^5.0.2"
|
"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": {
|
"devDependencies": {
|
||||||
"@f0x52/eslint-config-react": "^1.1.0",
|
"@f0x52/eslint-config-react": "^1.1.0",
|
||||||
|
|
|
@ -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: <a href="https://liberapay.com/f0x/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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 (
|
|
||||||
<React.Fragment key={block.id}>
|
|
||||||
<div><input type="checkbox" onChange={update} checked={checked.has(block.id)}></input></div>
|
|
||||||
<div>{block.domain}</div>
|
|
||||||
<div>{(new Date(block.created_at)).toLocaleString()}</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function clearChecked() {
|
|
||||||
setChecked(new Set());
|
|
||||||
}
|
|
||||||
|
|
||||||
function undoChecked() {
|
|
||||||
let amount = checked.size;
|
|
||||||
if(confirm(`Are you sure you want to remove ${amount} block(s)?`)) {
|
|
||||||
setInfo("");
|
|
||||||
Promise.map(Array.from(checked.values()), (block) => {
|
|
||||||
console.log("deleting", block);
|
|
||||||
return oauth.apiRequest(`/api/v1/admin/domain_blocks/${block}`, "DELETE");
|
|
||||||
}).then((res) => {
|
|
||||||
console.log(res);
|
|
||||||
setInfo(`Deleted ${amount} blocks: ${res.map((a) => a.domain).join(", ")}`);
|
|
||||||
}).catch((e) => {
|
|
||||||
setError(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
let newBlocks = blocks.filter((block) => {
|
|
||||||
if (checked.size > 0 && checked.has(block.id)) {
|
|
||||||
checked.delete(block.id);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setBlocks(newBlocks);
|
|
||||||
clearChecked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="blocks">
|
|
||||||
<h1>Blocks</h1>
|
|
||||||
<div className="error accent">{errorMsg}</div>
|
|
||||||
<div>{info}</div>
|
|
||||||
<AddBlock oauth={oauth} blocks={blocks} setBlocks={setBlocks} />
|
|
||||||
<h3>Blocks:</h3>
|
|
||||||
<div style={{display: "grid", gridTemplateColumns: "1fr auto"}}>
|
|
||||||
<span onClick={clearChecked} className="accent" style={{alignSelf: "end"}}>uncheck all</span>
|
|
||||||
<button onClick={undoChecked}>Unblock selected</button>
|
|
||||||
</div>
|
|
||||||
<div className="blocklist overflow">
|
|
||||||
{blockList}
|
|
||||||
</div>
|
|
||||||
<BulkBlocking oauth={oauth} blocks={blocks} setBlocks={setBlocks}/>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function BulkBlocking({oauth, blocks, setBlocks}) {
|
|
||||||
const [bulk, setBulk] = React.useState("");
|
|
||||||
const [blockMap, setBlockMap] = React.useState(new Map());
|
|
||||||
const [output, setOutput] = React.useState();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
let newBlockMap = new Map();
|
|
||||||
blocks.forEach((block) => {
|
|
||||||
newBlockMap.set(block.domain, block);
|
|
||||||
});
|
|
||||||
setBlockMap(newBlockMap);
|
|
||||||
}, [blocks]);
|
|
||||||
|
|
||||||
const fileRef = React.useRef();
|
|
||||||
|
|
||||||
function error(e) {
|
|
||||||
setOutput(<div className="error accent">{e}</div>);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileUpload() {
|
|
||||||
let reader = new FileReader();
|
|
||||||
reader.addEventListener("load", (e) => {
|
|
||||||
try {
|
|
||||||
// TODO: use validatem?
|
|
||||||
let json = JSON.parse(e.target.result);
|
|
||||||
json.forEach((block) => {
|
|
||||||
console.log("block:", block);
|
|
||||||
});
|
|
||||||
} catch(e) {
|
|
||||||
error(e.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
reader.readAsText(fileRef.current.files[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (fileRef && fileRef.current) {
|
|
||||||
fileRef.current.addEventListener("change", fileUpload);
|
|
||||||
}
|
|
||||||
return function cleanup() {
|
|
||||||
fileRef.current.removeEventListener("change", fileUpload);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function textImport() {
|
|
||||||
Promise.try(() => {
|
|
||||||
if (bulk[0] == "[") {
|
|
||||||
// assume it's json
|
|
||||||
return JSON.parse(bulk);
|
|
||||||
} else {
|
|
||||||
return bulk.split("\n").map((val) => {
|
|
||||||
return {
|
|
||||||
domain: val.trim()
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).then((domains) => {
|
|
||||||
console.log(domains);
|
|
||||||
let before = domains.length;
|
|
||||||
setOutput(`Importing ${before} domain(s)`);
|
|
||||||
domains = domains.filter(({domain}) => {
|
|
||||||
return (domain != "" && !blockMap.has(domain));
|
|
||||||
});
|
|
||||||
setOutput(<span>{output}<br/>{`Deduplicated ${before - domains.length}/${before} with existing blocks, adding ${domains.length} block(s)`}</span>);
|
|
||||||
if (domains.length > 0) {
|
|
||||||
let data = new FormData();
|
|
||||||
data.append("domains", new Blob([JSON.stringify(domains)], {type: "application/json"}), "import.json");
|
|
||||||
return oauth.apiRequest("/api/v1/admin/domain_blocks?import=true", "POST", data, "form");
|
|
||||||
}
|
|
||||||
}).then((json) => {
|
|
||||||
console.log("bulk import result:", json);
|
|
||||||
setBlocks(sortBlocks(deduplicateBlocks([...json, ...blocks])));
|
|
||||||
}).catch((e) => {
|
|
||||||
error(e.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function textExport() {
|
|
||||||
setBulk(blocks.reduce((str, val) => {
|
|
||||||
if (typeof str == "object") {
|
|
||||||
return str.domain;
|
|
||||||
} else {
|
|
||||||
return str + "\n" + val.domain;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function jsonExport() {
|
|
||||||
Promise.try(() => {
|
|
||||||
return oauth.apiRequest("/api/v1/admin/domain_blocks?export=true", "GET");
|
|
||||||
}).then((json) => {
|
|
||||||
fileDownload(JSON.stringify(json), "block-export.json");
|
|
||||||
}).catch((e) => {
|
|
||||||
error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function textAreaUpdate(e) {
|
|
||||||
setBulk(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<h3>Bulk import/export</h3>
|
|
||||||
<label htmlFor="bulk">Domains, one per line:</label>
|
|
||||||
<textarea value={bulk} rows={20} onChange={textAreaUpdate}></textarea>
|
|
||||||
<div className="controls">
|
|
||||||
<button onClick={textImport}>Import All From Field</button>
|
|
||||||
<button onClick={textExport}>Export To Field</button>
|
|
||||||
<label className="button" htmlFor="upload">Upload .json</label>
|
|
||||||
<button onClick={jsonExport}>Download .json</button>
|
|
||||||
</div>
|
|
||||||
{output}
|
|
||||||
<input type="file" id="upload" className="hidden" ref={fileRef}></input>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddBlock({oauth, blocks, setBlocks}) {
|
|
||||||
const [domain, setDomain] = React.useState("");
|
|
||||||
const [type, setType] = React.useState("suspend");
|
|
||||||
const [obfuscated, setObfuscated] = React.useState(false);
|
|
||||||
const [privateDescription, setPrivateDescription] = React.useState("");
|
|
||||||
const [publicDescription, setPublicDescription] = React.useState("");
|
|
||||||
|
|
||||||
function addBlock() {
|
|
||||||
console.log(`${type}ing`, domain);
|
|
||||||
Promise.try(() => {
|
|
||||||
return oauth.apiRequest("/api/v1/admin/domain_blocks", "POST", {
|
|
||||||
domain: domain,
|
|
||||||
obfuscate: obfuscated,
|
|
||||||
private_comment: privateDescription,
|
|
||||||
public_comment: publicDescription
|
|
||||||
}, "json");
|
|
||||||
}).then((json) => {
|
|
||||||
setDomain("");
|
|
||||||
setPrivateDescription("");
|
|
||||||
setPublicDescription("");
|
|
||||||
setBlocks([json, ...blocks]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDomainChange(e) {
|
|
||||||
setDomain(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTypeChange(e) {
|
|
||||||
setType(e.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKeyDown(e) {
|
|
||||||
if (e.key == "Enter") {
|
|
||||||
addBlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<h3>Add Block:</h3>
|
|
||||||
<div className="addblock">
|
|
||||||
<input id="domain" placeholder="instance" onChange={onDomainChange} value={domain} onKeyDown={onKeyDown} />
|
|
||||||
<select value={type} onChange={onTypeChange}>
|
|
||||||
<option id="suspend">Suspend</option>
|
|
||||||
<option id="silence">Silence</option>
|
|
||||||
</select>
|
|
||||||
<button onClick={addBlock}>Add</button>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="private">Private description:</label><br/>
|
|
||||||
<textarea id="private" value={privateDescription} onChange={(e) => setPrivateDescription(e.target.value)}></textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="public">Public description:</label><br/>
|
|
||||||
<textarea id="public" value={publicDescription} onChange={(e) => setPublicDescription(e.target.value)}></textarea>
|
|
||||||
</div>
|
|
||||||
<div className="single">
|
|
||||||
<label htmlFor="obfuscate">Obfuscate:</label>
|
|
||||||
<input id="obfuscate" type="checkbox" value={obfuscated} onChange={(e) => setObfuscated(e.target.checked)}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// function Blocklist() {
|
|
||||||
// return (
|
|
||||||
// <section className="blocklists">
|
|
||||||
// <h1>Blocklists</h1>
|
|
||||||
// </section>
|
|
||||||
// );
|
|
||||||
// }
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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 (
|
|
||||||
<React.Fragment>
|
|
||||||
<Logout oauth={oauth}/>
|
|
||||||
<Settings oauth={oauth} />
|
|
||||||
<Blocks oauth={oauth}/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Logout({oauth}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={oauth.logout}>Logout</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createPanel("GoToSocial Admin Panel", ["admin"], AdminPanel);
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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 (
|
|
||||||
<section className="info login">
|
|
||||||
<h1>Instance Information <button onClick={submit}>Save</button></h1>
|
|
||||||
<div className="error accent">
|
|
||||||
{errorMsg}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{statusMsg}
|
|
||||||
</div>
|
|
||||||
<form onSubmit={(e) => e.preventDefault()}>
|
|
||||||
{editableObject(info)}
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function editableObject(obj, path=[]) {
|
|
||||||
const readOnlyKeys = ["uri", "version", "urls_streaming_api", "stats"];
|
|
||||||
const hiddenKeys = ["contact_account_", "urls"];
|
|
||||||
const explicitShownKeys = ["contact_account_username"];
|
|
||||||
const implementedKeys = "title, contact_account_username, email, short_description, description, terms, avatar, header".split(", ");
|
|
||||||
const textareaKeys = ["short_description", "description"]
|
|
||||||
|
|
||||||
let listing = Object.entries(obj).map(([key, val]) => {
|
|
||||||
let fullkey = [...path, key].join("_");
|
|
||||||
|
|
||||||
if (
|
|
||||||
hiddenKeys.includes(fullkey) ||
|
|
||||||
hiddenKeys.includes(path.join("_")+"_") // also match just parent path
|
|
||||||
) {
|
|
||||||
if (!explicitShownKeys.includes(fullkey)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
// FIXME: handle this
|
|
||||||
} else if (typeof val == "object") {
|
|
||||||
return (<React.Fragment key={fullkey}>
|
|
||||||
{editableObject(val, [...path, key])}
|
|
||||||
</React.Fragment>);
|
|
||||||
}
|
|
||||||
|
|
||||||
let isImplemented = "";
|
|
||||||
if (!implementedKeys.includes(fullkey)) {
|
|
||||||
isImplemented = " notImplemented";
|
|
||||||
}
|
|
||||||
|
|
||||||
let isReadOnly = (
|
|
||||||
readOnlyKeys.includes(fullkey) ||
|
|
||||||
readOnlyKeys.includes(path.join("_")) ||
|
|
||||||
isImplemented != ""
|
|
||||||
);
|
|
||||||
|
|
||||||
let label = key.replace(/_/g, " ");
|
|
||||||
if (path.length > 0) {
|
|
||||||
label = `\u00A0`.repeat(4 * path.length) + label;
|
|
||||||
}
|
|
||||||
|
|
||||||
let inputProps;
|
|
||||||
let changeFunc;
|
|
||||||
if (val === true || val === false) {
|
|
||||||
inputProps = {
|
|
||||||
type: "checkbox",
|
|
||||||
defaultChecked: val,
|
|
||||||
disabled: isReadOnly
|
|
||||||
};
|
|
||||||
changeFunc = (e) => e.target.checked;
|
|
||||||
} else if (val.length != 0 && !isNaN(val)) {
|
|
||||||
inputProps = {
|
|
||||||
type: "number",
|
|
||||||
defaultValue: val,
|
|
||||||
readOnly: isReadOnly
|
|
||||||
};
|
|
||||||
changeFunc = (e) => e.target.value;
|
|
||||||
} else {
|
|
||||||
inputProps = {
|
|
||||||
type: "text",
|
|
||||||
defaultValue: val,
|
|
||||||
readOnly: isReadOnly
|
|
||||||
};
|
|
||||||
changeFunc = (e) => e.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRef(element) {
|
|
||||||
if (element != null) {
|
|
||||||
element.addEventListener("change", (e) => {
|
|
||||||
obj[key] = changeFunc(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let field;
|
|
||||||
if (textareaKeys.includes(fullkey)) {
|
|
||||||
field = <textarea className={isImplemented} ref={setRef} {...inputProps}></textarea>
|
|
||||||
} else {
|
|
||||||
field = <input className={isImplemented} ref={setRef} {...inputProps} />
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<React.Fragment key={fullkey}>
|
|
||||||
<label htmlFor={key} className="capitalize">{label}</label>
|
|
||||||
<div className={isImplemented}>
|
|
||||||
{field}
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
{path != "" &&
|
|
||||||
<><b>{path}:</b> <span id="filler"></span></>
|
|
||||||
}
|
|
||||||
{listing}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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 (
|
|
||||||
<section className="basic">
|
|
||||||
<h1>@{account.username}'s Profile Info</h1>
|
|
||||||
<form>
|
|
||||||
<div className="labelinput">
|
|
||||||
<label htmlFor="header">Header</label>
|
|
||||||
<div className="border">
|
|
||||||
<img className="headerpreview" src={headerSrc} alt={headerSrc ? `header image for ${account.username}` : "None set"}/>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="header" className="file-input button">Browse…</label>
|
|
||||||
<span>{headerFile ? headerFile.name : ""}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input className="hidden" id="header" type="file" accept="image/*" onChange={headerOnChange}/>
|
|
||||||
</div>
|
|
||||||
<div className="labelinput">
|
|
||||||
<label htmlFor="avatar">Avatar</label>
|
|
||||||
<div className="border">
|
|
||||||
<img className="avatarpreview" src={avatarSrc} alt={headerSrc ? `avatar image for ${account.username}` : "None set"}/>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="avatar" className="file-input button">Browse…</label>
|
|
||||||
<span>{avatarFile ? avatarFile.name : ""}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input className="hidden" id="avatar" type="file" accept="image/*" onChange={avatarOnChange}/>
|
|
||||||
</div>
|
|
||||||
<div className="labelinput">
|
|
||||||
<label htmlFor="displayname">Display Name</label>
|
|
||||||
<input id="displayname" type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="A GoToSocial user"/>
|
|
||||||
</div>
|
|
||||||
<div className="labelinput">
|
|
||||||
<label htmlFor="bio">Bio</label>
|
|
||||||
<textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."/>
|
|
||||||
</div>
|
|
||||||
{ !allowCustomCSS ? null :
|
|
||||||
<div className="labelinput">
|
|
||||||
<label htmlFor="customcss">Custom CSS</label>
|
|
||||||
<textarea className="mono" id="customcss" value={customCSS} onChange={(e) => setCustomCSS(e.target.value)}/>
|
|
||||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom CSS (opens in a new tab)</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className="labelcheckbox">
|
|
||||||
<label htmlFor="locked">Manually approve follow requests</label>
|
|
||||||
<input id="locked" type="checkbox" checked={locked} onChange={(e) => setLocked(e.target.checked)}/>
|
|
||||||
</div>
|
|
||||||
<Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg}/>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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 (
|
|
||||||
<React.Fragment>
|
|
||||||
<div>
|
|
||||||
<button className="logout" onClick={oauth.logout}>Log out of settings panel</button>
|
|
||||||
</div>
|
|
||||||
<Basic oauth={oauth} account={account} allowCustomCSS={allowCustomCSS}/>
|
|
||||||
<Posts oauth={oauth} account={account}/>
|
|
||||||
<Security oauth={oauth}/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createPanel("GoToSocial User Panel", ["read write"], UserPanel);
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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 (
|
|
||||||
<section className="posts">
|
|
||||||
<h1>Post Settings</h1>
|
|
||||||
<form>
|
|
||||||
<div className="labelselect">
|
|
||||||
<label htmlFor="language">Default post language</label>
|
|
||||||
<select id="language" autoComplete="language" value={language} onChange={(e) => setLanguage(e.target.value)}>
|
|
||||||
<Languages />
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="labelselect">
|
|
||||||
<label htmlFor="privacy">Default post privacy</label>
|
|
||||||
<select id="privacy" value={privacy} onChange={(e) => setPrivacy(e.target.value)}>
|
|
||||||
<option value="private">Private / followers-only)</option>
|
|
||||||
<option value="unlisted">Unlisted</option>
|
|
||||||
<option value="public">Public</option>
|
|
||||||
</select>
|
|
||||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a>
|
|
||||||
</div>
|
|
||||||
<div className="labelselect">
|
|
||||||
<label htmlFor="format">Default post format</label>
|
|
||||||
<select id="format" value={format} onChange={(e) => setFormat(e.target.value)}>
|
|
||||||
<option value="plain">Plain (default)</option>
|
|
||||||
<option value="markdown">Markdown</option>
|
|
||||||
</select>
|
|
||||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a>
|
|
||||||
</div>
|
|
||||||
<div className="labelcheckbox">
|
|
||||||
<label htmlFor="sensitive">Mark my posts as sensitive by default</label>
|
|
||||||
<input id="sensitive" type="checkbox" checked={sensitive} onChange={(e) => setSensitive(e.target.checked)}/>
|
|
||||||
</div>
|
|
||||||
<Submit onClick={submit} label="Save post settings" errorMsg={errorMsg} statusMsg={statusMsg}/>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"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 (
|
|
||||||
<section className="security">
|
|
||||||
<h1>Password Change</h1>
|
|
||||||
<form>
|
|
||||||
<div className="labelinput">
|
|
||||||
<label htmlFor="password">Current password</label>
|
|
||||||
<input name="password" id="password" type="password" autoComplete="current-password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<div className="labelinput">
|
|
||||||
<label htmlFor="new-password">New password</label>
|
|
||||||
<input name="new-password" id="new-password" type="password" autoComplete="new-password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<div className="labelinput">
|
|
||||||
<label htmlFor="confirm-new-password">Confirm new password</label>
|
|
||||||
<input name="confirm-new-password" id="confirm-new-password" type="password" autoComplete="new-password" value={newPasswordConfirm} onChange={(e) => setNewPasswordConfirm(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<Submit onClick={submit} label="Save new password" errorMsg={errorMsg} statusMsg={statusMsg}/>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
61
web/source/settings-panel/admin/actions.js
Normal file
61
web/source/settings-panel/admin/actions.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
<>
|
||||||
|
<h1>Admin Actions</h1>
|
||||||
|
<div>
|
||||||
|
<h2>Media cleanup</h2>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="days">Days: </label>
|
||||||
|
<input id="days" type="number" value={days} onChange={(e) => setDays(e.target.value)}/>
|
||||||
|
</div>
|
||||||
|
<Submit onClick={removeMedia} label="Remove media" errorMsg={errorMsg} statusMsg={statusMsg} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
212
web/source/settings-panel/admin/emoji.js
Normal file
212
web/source/settings-panel/admin/emoji.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
<Switch>
|
||||||
|
<Route path={`${base}/:emojiId`}>
|
||||||
|
<EmojiDetailWrapped />
|
||||||
|
</Route>
|
||||||
|
<EmojiOverview />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<h1>Custom Emoji</h1>
|
||||||
|
Loading...
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Custom Emoji</h1>
|
||||||
|
<EmojiList/>
|
||||||
|
<NewEmoji/>
|
||||||
|
{errorMsg.length > 0 &&
|
||||||
|
<div className="error accent">{errorMsg}</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NewEmojiForm = formFields(adminActions.updateNewEmojiVal, (state) => state.admin.newEmoji);
|
||||||
|
function NewEmoji() {
|
||||||
|
const dispatch = Redux.useDispatch();
|
||||||
|
const newEmojiForm = Redux.useSelector((state) => state.admin.newEmoji);
|
||||||
|
|
||||||
|
const [errorMsg, setError] = React.useState("");
|
||||||
|
const [statusMsg, setStatus] = React.useState("");
|
||||||
|
|
||||||
|
const uploadEmoji = submit(
|
||||||
|
() => dispatch(api.admin.newEmoji()),
|
||||||
|
{
|
||||||
|
setStatus, setError,
|
||||||
|
onSuccess: function() {
|
||||||
|
URL.revokeObjectURL(newEmojiForm.image);
|
||||||
|
return Promise.all([
|
||||||
|
dispatch(adminActions.updateNewEmojiVal(["image", undefined])),
|
||||||
|
dispatch(adminActions.updateNewEmojiVal(["imageFile", undefined])),
|
||||||
|
dispatch(adminActions.updateNewEmojiVal(["shortcode", ""])),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (newEmojiForm.shortcode.length == 0) {
|
||||||
|
if (newEmojiForm.imageFile != undefined) {
|
||||||
|
let [name, ext] = newEmojiForm.imageFile.name.split(".");
|
||||||
|
dispatch(adminActions.updateNewEmojiVal(["shortcode", name]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let emojiOrShortcode = `:${newEmojiForm.shortcode}:`;
|
||||||
|
|
||||||
|
if (newEmojiForm.image != undefined) {
|
||||||
|
emojiOrShortcode = <img
|
||||||
|
className="emoji"
|
||||||
|
src={newEmojiForm.image}
|
||||||
|
title={`:${newEmojiForm.shortcode}:`}
|
||||||
|
alt={newEmojiForm.shortcode}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Add new custom emoji</h2>
|
||||||
|
|
||||||
|
<FakeToot>
|
||||||
|
Look at this new custom emoji {emojiOrShortcode} isn't it cool?
|
||||||
|
</FakeToot>
|
||||||
|
|
||||||
|
<NewEmojiForm.File
|
||||||
|
id="image"
|
||||||
|
name="Image"
|
||||||
|
fileType="image/png,image/gif"
|
||||||
|
showSize={true}
|
||||||
|
maxSize={50 * 1000}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewEmojiForm.TextInput
|
||||||
|
id="shortcode"
|
||||||
|
name="Shortcode (without : :), must be unique on the instance"
|
||||||
|
placeHolder="blobcat"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Submit onClick={uploadEmoji} label="Upload" errorMsg={errorMsg} statusMsg={statusMsg} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiList() {
|
||||||
|
const emoji = Redux.useSelector((state) => state.admin.emoji);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Overview</h2>
|
||||||
|
<div className="list emoji-list">
|
||||||
|
{Object.entries(emoji).map(([category, entries]) => {
|
||||||
|
return <EmojiCategory key={category} category={category} entries={entries}/>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiCategory({category, entries}) {
|
||||||
|
return (
|
||||||
|
<div className="entry">
|
||||||
|
<b>{category}</b>
|
||||||
|
<div className="emoji-group">
|
||||||
|
{entries.map((e) => {
|
||||||
|
return (
|
||||||
|
// <Link key={e.static_url} to={`${base}/${e.shortcode}`}>
|
||||||
|
<Link key={e.static_url} to={`${base}`}>
|
||||||
|
<a>
|
||||||
|
<img src={e.static_url} alt={e.shortcode} title={`:${e.shortcode}:`}/>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <EmojiDetail id={emojiId} Form={fields} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiDetail({id, Form}) {
|
||||||
|
return (
|
||||||
|
"Not implemented yet"
|
||||||
|
);
|
||||||
|
}
|
382
web/source/settings-panel/admin/federation.js
Normal file
382
web/source/settings-panel/admin/federation.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
<div>
|
||||||
|
<h1>Federation</h1>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route path={`${base}/:domain`}>
|
||||||
|
<InstancePageWrapped />
|
||||||
|
</Route>
|
||||||
|
<InstanceOverview />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function InstanceOverview() {
|
||||||
|
const [filter, setFilter] = React.useState("");
|
||||||
|
const blockedInstances = Redux.useSelector(state => state.admin.blockedInstances);
|
||||||
|
const [_location, setLocation] = useLocation();
|
||||||
|
|
||||||
|
function filterFormSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
setLocation(`${base}/${filter}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Federation</h1>
|
||||||
|
Here you can see an overview of blocked instances.
|
||||||
|
|
||||||
|
<div className="instance-list">
|
||||||
|
<h2>Blocked instances</h2>
|
||||||
|
<form action={`${base}/view`} className="filter" role="search" onSubmit={filterFormSubmit}>
|
||||||
|
<input name="domain" value={filter} onChange={(e) => setFilter(e.target.value)}/>
|
||||||
|
<Link to={`${base}/${filter}`}><a className="button">Add block</a></Link>
|
||||||
|
</form>
|
||||||
|
<div className="list">
|
||||||
|
{Object.values(blockedInstances).filter((a) => a.domain.startsWith(filter)).map((entry) => {
|
||||||
|
return (
|
||||||
|
<Link key={entry.domain} to={`${base}/${entry.domain}`}>
|
||||||
|
<a className="entry nounderline">
|
||||||
|
<span id="domain">
|
||||||
|
{entry.domain}
|
||||||
|
</span>
|
||||||
|
<span id="date">
|
||||||
|
{new Date(entry.created_at).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BulkBlocking/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bulk = formFields(adminActions.updateBulkBlockVal, (state) => state.admin.bulkBlock);
|
||||||
|
function BulkBlocking() {
|
||||||
|
const dispatch = Redux.useDispatch();
|
||||||
|
const {bulkBlock, blockedInstances} = Redux.useSelector(state => state.admin);
|
||||||
|
|
||||||
|
const [errorMsg, setError] = React.useState("");
|
||||||
|
const [statusMsg, setStatus] = React.useState("");
|
||||||
|
|
||||||
|
function importBlocks() {
|
||||||
|
setStatus("Processing");
|
||||||
|
setError("");
|
||||||
|
return Promise.try(() => {
|
||||||
|
return dispatch(api.admin.bulkDomainBlock());
|
||||||
|
}).then(({success, invalidDomains}) => {
|
||||||
|
return Promise.try(() => {
|
||||||
|
return resetBulk();
|
||||||
|
}).then(() => {
|
||||||
|
dispatch(adminActions.updateBulkBlockVal(["list", invalidDomains.join("\n")]));
|
||||||
|
|
||||||
|
let stat = "";
|
||||||
|
if (success == 0) {
|
||||||
|
return setError("No valid domains in import");
|
||||||
|
} else if (success == 1) {
|
||||||
|
stat = "Imported 1 domain";
|
||||||
|
} else {
|
||||||
|
stat = `Imported ${success} domains`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidDomains.length > 0) {
|
||||||
|
if (invalidDomains.length == 1) {
|
||||||
|
stat += ", input contained 1 invalid domain.";
|
||||||
|
} else {
|
||||||
|
stat += `, input contained ${invalidDomains.length} invalid domains.`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stat += "!";
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(stat);
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
setError(e.message);
|
||||||
|
setStatus("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportBlocks() {
|
||||||
|
return Promise.try(() => {
|
||||||
|
setStatus("Exporting");
|
||||||
|
setError("");
|
||||||
|
let asJSON = bulkBlock.exportType.startsWith("json");
|
||||||
|
let _asCSV = bulkBlock.exportType.startsWith("csv");
|
||||||
|
|
||||||
|
let exportList = Object.values(blockedInstances).map((entry) => {
|
||||||
|
if (asJSON) {
|
||||||
|
return {
|
||||||
|
domain: entry.domain,
|
||||||
|
public_comment: entry.public_comment
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return entry.domain;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bulkBlock.exportType == "json") {
|
||||||
|
return dispatch(adminActions.updateBulkBlockVal(["list", JSON.stringify(exportList)]));
|
||||||
|
} else if (bulkBlock.exportType == "json-download") {
|
||||||
|
return fileDownload(JSON.stringify(exportList), "block-export.json");
|
||||||
|
} else if (bulkBlock.exportType == "plain") {
|
||||||
|
return dispatch(adminActions.updateBulkBlockVal(["list", exportList.join("\n")]));
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
setStatus("Exported!");
|
||||||
|
}).catch((e) => {
|
||||||
|
setError(e.message);
|
||||||
|
setStatus("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBulk(e) {
|
||||||
|
if (e != undefined) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
return dispatch(adminActions.resetBulkBlockVal());
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableInfoFields(props={}) {
|
||||||
|
if (bulkBlock.list[0] == "[") {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
disabled: true,
|
||||||
|
placeHolder: "Domain list is a JSON import, input disabled"
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bulk">
|
||||||
|
<h2>Import / Export <a onClick={resetBulk}>reset</a></h2>
|
||||||
|
<Bulk.TextArea
|
||||||
|
id="list"
|
||||||
|
name="Domains, one per line"
|
||||||
|
placeHolder={`google.com\nfacebook.com`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Bulk.TextArea
|
||||||
|
id="public_comment"
|
||||||
|
name="Public comment"
|
||||||
|
inputProps={disableInfoFields({rows: 3})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Bulk.TextArea
|
||||||
|
id="private_comment"
|
||||||
|
name="Private comment"
|
||||||
|
inputProps={disableInfoFields({rows: 3})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Bulk.Checkbox
|
||||||
|
id="obfuscate"
|
||||||
|
name="Obfuscate domains? "
|
||||||
|
inputProps={disableInfoFields()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="hidden">
|
||||||
|
<Bulk.File
|
||||||
|
id="json"
|
||||||
|
fileType="application/json"
|
||||||
|
withPreview={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="messagebutton">
|
||||||
|
<div>
|
||||||
|
<button type="submit" onClick={importBlocks}>Import</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" onClick={exportBlocks}>Export</button>
|
||||||
|
|
||||||
|
<Bulk.Select id="exportType" name="Export type" options={
|
||||||
|
<>
|
||||||
|
<option value="plain">One per line in text field</option>
|
||||||
|
<option value="json">JSON in text field</option>
|
||||||
|
<option value="json-download">JSON file download</option>
|
||||||
|
<option disabled value="csv">CSV in text field (glitch-soc)</option>
|
||||||
|
<option disabled value="csv-download">CSV file download (glitch-soc)</option>
|
||||||
|
</>
|
||||||
|
}/>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<div>
|
||||||
|
{errorMsg.length > 0 &&
|
||||||
|
<div className="error accent">{errorMsg}</div>
|
||||||
|
}
|
||||||
|
{statusMsg.length > 0 &&
|
||||||
|
<div className="accent">{statusMsg}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BackButton() {
|
||||||
|
return (
|
||||||
|
<Link to={base}>
|
||||||
|
<a className="button">< back</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstancePageWrapped() {
|
||||||
|
/* 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, {domain}] = useRoute(`${base}/:domain`);
|
||||||
|
|
||||||
|
if (domain == "view") { // from form field submission
|
||||||
|
let realDomain = (new URL(document.location)).searchParams.get("domain");
|
||||||
|
if (realDomain == undefined) {
|
||||||
|
return <Redirect to={base}/>;
|
||||||
|
} else {
|
||||||
|
domain = realDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function alterDomain([key, val]) {
|
||||||
|
return adminActions.updateDomainBlockVal([domain, key, val]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = formFields(alterDomain, (state) => state.admin.newInstanceBlocks[domain]);
|
||||||
|
|
||||||
|
return <InstancePage domain={domain} Form={fields} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstancePage({domain, Form}) {
|
||||||
|
const dispatch = Redux.useDispatch();
|
||||||
|
const entry = Redux.useSelector(state => state.admin.newInstanceBlocks[domain]);
|
||||||
|
const [_location, setLocation] = useLocation();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (entry == undefined) {
|
||||||
|
dispatch(api.admin.getEditableDomainBlock(domain));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [errorMsg, setError] = React.useState("");
|
||||||
|
const [statusMsg, setStatus] = React.useState("");
|
||||||
|
|
||||||
|
if (entry == undefined) {
|
||||||
|
return "Loading...";
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBlock = submit(
|
||||||
|
() => dispatch(api.admin.updateDomainBlock(domain)),
|
||||||
|
{setStatus, setError}
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeBlock = submit(
|
||||||
|
() => dispatch(api.admin.removeDomainBlock(domain)),
|
||||||
|
{setStatus, setError, startStatus: "Removing", successStatus: "Removed!", onSuccess: () => {
|
||||||
|
setLocation(base);
|
||||||
|
}}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1><BackButton/> Federation settings for: {domain}</h1>
|
||||||
|
{entry.new && "No stored block yet, you can add one below:"}
|
||||||
|
|
||||||
|
<Form.TextArea
|
||||||
|
id="public_comment"
|
||||||
|
name="Public comment"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.TextArea
|
||||||
|
id="private_comment"
|
||||||
|
name="Private comment"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Checkbox
|
||||||
|
id="obfuscate"
|
||||||
|
name="Obfuscate domain? "
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="messagebutton">
|
||||||
|
<button type="submit" onClick={updateBlock}>{entry.new ? "Add block" : "Save block"}</button>
|
||||||
|
|
||||||
|
{!entry.new &&
|
||||||
|
<button className="danger" onClick={removeBlock}>Remove block</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
{errorMsg.length > 0 &&
|
||||||
|
<div className="error accent">{errorMsg}</div>
|
||||||
|
}
|
||||||
|
{statusMsg.length > 0 &&
|
||||||
|
<div className="accent">{statusMsg}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
110
web/source/settings-panel/admin/settings.js
Normal file
110
web/source/settings-panel/admin/settings.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
<div>
|
||||||
|
<h1>Instance Settings</h1>
|
||||||
|
<TextInput
|
||||||
|
id="title"
|
||||||
|
name="Title"
|
||||||
|
placeHolder="My GoToSocial instance"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id="short_description"
|
||||||
|
name="Short description"
|
||||||
|
placeHolder="A small testing instance for the GoToSocial alpha."
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="Full description"
|
||||||
|
placeHolder="A small testing instance for the GoToSocial alpha."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="contact_account.username"
|
||||||
|
name="Contact user (local account username)"
|
||||||
|
placeHolder="admin"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
id="email"
|
||||||
|
name="Contact email"
|
||||||
|
placeHolder="admin@example.com"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id="terms"
|
||||||
|
name="Terms & Conditions"
|
||||||
|
placeHolder=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <div className="file-upload">
|
||||||
|
<h3>Instance avatar</h3>
|
||||||
|
<div>
|
||||||
|
<img className="preview avatar" src={instance.avatar} alt={instance.avatar ? `Avatar image for the instance` : "No instance avatar image set"} />
|
||||||
|
<File
|
||||||
|
id="avatar"
|
||||||
|
fileType="image/*"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="file-upload">
|
||||||
|
<h3>Instance header</h3>
|
||||||
|
<div>
|
||||||
|
<img className="preview header" src={instance.header} alt={instance.header ? `Header image for the instance` : "No instance header image set"} />
|
||||||
|
<File
|
||||||
|
id="header"
|
||||||
|
fileType="image/*"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<Submit onClick={updateSettings} label="Save" errorMsg={errorMsg} statusMsg={statusMsg} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
45
web/source/settings-panel/components/error.jsx
Normal file
45
web/source/settings-panel/components/error.jsx
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Promise = require("bluebird");
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
module.exports = function ErrorFallback({error, resetErrorBoundary}) {
|
||||||
|
return (
|
||||||
|
<div className="error">
|
||||||
|
<p>
|
||||||
|
{"An error occured, please report this on the "}
|
||||||
|
<a href="https://github.com/superseriousbusiness/gotosocial/issues">GoToSocial issue tracker</a>
|
||||||
|
{" or "}
|
||||||
|
<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>.
|
||||||
|
<br/>Include the details below:
|
||||||
|
</p>
|
||||||
|
<pre>
|
||||||
|
{error.name}: {error.message}
|
||||||
|
</pre>
|
||||||
|
<pre>
|
||||||
|
{error.stack}
|
||||||
|
</pre>
|
||||||
|
<p>
|
||||||
|
<button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
43
web/source/settings-panel/components/fake-toot.jsx
Normal file
43
web/source/settings-panel/components/fake-toot.jsx
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
<div className="toot expanded">
|
||||||
|
<div className="contentgrid">
|
||||||
|
<span className="avatar">
|
||||||
|
<img src={account.avatar} alt=""/>
|
||||||
|
</span>
|
||||||
|
<span className="displayname">{account.display_name.trim().length > 0 ? account.display_name : account.username}</span>
|
||||||
|
<span className="username">@{account.username}</span>
|
||||||
|
<div className="text">
|
||||||
|
<div className="content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
167
web/source/settings-panel/components/form-fields.jsx
Normal file
167
web/source/settings-panel/components/form-fields.jsx
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 = <input type="text" id={id} value={get(state, id, "")} placeholder={placeHolder} className={className} onChange={onTextChange(id)} {...inputProps}/>;
|
||||||
|
} else if (type == "textarea") {
|
||||||
|
field = <textarea type="text" id={id} value={get(state, id, "")} placeholder={placeHolder} className={className} onChange={onTextChange(id)} rows={8} {...inputProps}/>;
|
||||||
|
} else if (type == "checkbox") {
|
||||||
|
field = <input type="checkbox" id={id} checked={get(state, id, false)} className={className} onChange={onCheckChange(id)} {...inputProps}/>;
|
||||||
|
} else if (type == "select") {
|
||||||
|
field = (
|
||||||
|
<select id={id} value={get(state, id, "")} className={className} onChange={onTextChange(id)} {...inputProps}>
|
||||||
|
{options}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
} 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 = <span className="error-text">{size}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
field = (
|
||||||
|
<>
|
||||||
|
<label htmlFor={id} className="file-input button">Browse</label>
|
||||||
|
<span>
|
||||||
|
{file ? file.name : "no file selected"} {size}
|
||||||
|
</span>
|
||||||
|
{/* <a onClick={removeFile("header")}>remove</a> */}
|
||||||
|
<input className="hidden" id={id} type="file" accept={fileType} onChange={onFileChange(id, withPreview)} {...inputProps}/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
defaultLabel = false;
|
||||||
|
field = `unsupported FormField ${type}, this is a developer error`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = <label htmlFor={id}>{name}</label>;
|
||||||
|
return (
|
||||||
|
<div className={`form-field ${type}`}>
|
||||||
|
{defaultLabel ? label : null} {field}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
TextInput: function(props) {
|
||||||
|
return <FormField type="text" {...props} />;
|
||||||
|
},
|
||||||
|
|
||||||
|
TextArea: function(props) {
|
||||||
|
return <FormField type="textarea" {...props} />;
|
||||||
|
},
|
||||||
|
|
||||||
|
Checkbox: function(props) {
|
||||||
|
return <FormField type="checkbox" {...props} />;
|
||||||
|
},
|
||||||
|
|
||||||
|
Select: function(props) {
|
||||||
|
return <FormField type="select" {...props} />;
|
||||||
|
},
|
||||||
|
|
||||||
|
File: function(props) {
|
||||||
|
return <FormField type="file" {...props} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
eventListeners
|
||||||
|
};
|
102
web/source/settings-panel/components/login.jsx
Normal file
102
web/source/settings-panel/components/login.jsx
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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(
|
||||||
|
<>
|
||||||
|
<b>{e.type}</b>
|
||||||
|
<span>{e.message}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInstanceField(e) {
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
tryInstance(instanceField);
|
||||||
|
} else {
|
||||||
|
setInstanceField(e.target.value);
|
||||||
|
instanceFieldRef.current = e.target.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="login">
|
||||||
|
<h1>OAUTH Login:</h1>
|
||||||
|
{error}
|
||||||
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<label htmlFor="instance">Instance: </label>
|
||||||
|
<input value={instanceField} onChange={updateInstanceField} id="instance"/>
|
||||||
|
{errorMsg &&
|
||||||
|
<div className="error">
|
||||||
|
{errorMsg}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button onClick={tryInstance}>Authenticate</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
33
web/source/settings-panel/components/nav-button.jsx
Normal file
33
web/source/settings-panel/components/nav-button.jsx
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const { Link, useRoute } = require("wouter");
|
||||||
|
|
||||||
|
module.exports = function NavButton({href, name}) {
|
||||||
|
const [isActive] = useRoute(`${href}/:anything?`);
|
||||||
|
return (
|
||||||
|
<Link href={href}>
|
||||||
|
<a className={isActive ? "active" : ""} data-content={name}>
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
35
web/source/settings-panel/components/submit.jsx
Normal file
35
web/source/settings-panel/components/submit.jsx
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
module.exports = function Submit({onClick, label, errorMsg, statusMsg}) {
|
||||||
|
return (
|
||||||
|
<div className="messagebutton">
|
||||||
|
<button type="submit" onClick={onClick}>{ label }</button>
|
||||||
|
{errorMsg.length > 0 &&
|
||||||
|
<div className="error accent">{errorMsg}</div>
|
||||||
|
}
|
||||||
|
{statusMsg.length > 0 &&
|
||||||
|
<div className="accent">{statusMsg}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
178
web/source/settings-panel/index.js
Normal file
178
web/source/settings-panel/index.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 = (
|
||||||
|
<div className="error">
|
||||||
|
<b>{errorMsg.type}</b>
|
||||||
|
<span>{errorMsg.message}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogoutElement = (
|
||||||
|
<button className="logout" onClick={() => { dispatch(api.oauth.logout()); }}>
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (reduxTempStatus != undefined) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
{reduxTempStatus}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
} else if (tokenChecked && loginState == "login") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="sidebar">
|
||||||
|
{sidebar.all}
|
||||||
|
{isAdmin && sidebar.admin}
|
||||||
|
{LogoutElement}
|
||||||
|
</div>
|
||||||
|
<section className="with-sidebar">
|
||||||
|
{ErrorElement}
|
||||||
|
<Switch>
|
||||||
|
{panelRouter.all}
|
||||||
|
{isAdmin && panelRouter.admin}
|
||||||
|
<Route> {/* default route */}
|
||||||
|
<Redirect to="/settings/user" />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (loginState == "none") {
|
||||||
|
return (
|
||||||
|
<Login error={ErrorElement} />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let status;
|
||||||
|
|
||||||
|
if (loginState == "login") {
|
||||||
|
status = "Verifying stored login...";
|
||||||
|
} else if (loginState == "callback") {
|
||||||
|
status = "Processing OAUTH callback...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
{ErrorElement}
|
||||||
|
{LogoutElement}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function Main() {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<PersistGate loading={"loading..."} persistor={persistor}>
|
||||||
|
<App />
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = ReactDom.createRoot(document.getElementById("root"));
|
||||||
|
root.render(<React.StrictMode><Main /></React.StrictMode>);
|
192
web/source/settings-panel/lib/api/admin.js
Normal file
192
web/source/settings-panel/lib/api/admin.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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;
|
||||||
|
};
|
185
web/source/settings-panel/lib/api/index.js
Normal file
185
web/source/settings-panel/lib/api/index.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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
|
||||||
|
};
|
124
web/source/settings-panel/lib/api/oauth.js
Normal file
124
web/source/settings-panel/lib/api/oauth.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
67
web/source/settings-panel/lib/api/user.js
Normal file
67
web/source/settings-panel/lib/api/user.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
27
web/source/settings-panel/lib/errors.js
Normal file
27
web/source/settings-panel/lib/errors.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const createError = require("create-error");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
APIError: createError("APIError"),
|
||||||
|
OAUTHError: createError("OAUTHError"),
|
||||||
|
AuthenticationError: createError("AuthenticationError"),
|
||||||
|
};
|
102
web/source/settings-panel/lib/get-views.js
Normal file
102
web/source/settings-panel/lib/get-views.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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((
|
||||||
|
<Route path={`${url}/:page?`} key={url}>
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { }}>
|
||||||
|
{/* FIXME: implement onReset */}
|
||||||
|
<ViewComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Route>
|
||||||
|
));
|
||||||
|
|
||||||
|
links.push(
|
||||||
|
<NavButton key={url} href={url} name={name} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
panelRouterEl.push(
|
||||||
|
<Route key={base} path={base}>
|
||||||
|
<Redirect to={firstRoute} />
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
|
|
||||||
|
sidebarEl.push(
|
||||||
|
<React.Fragment key={name}>
|
||||||
|
<Link href={firstRoute}>
|
||||||
|
<a>
|
||||||
|
<h2>{name}</h2>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<nav>
|
||||||
|
{links}
|
||||||
|
</nav>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sidebar, panelRouter };
|
||||||
|
};
|
48
web/source/settings-panel/lib/submit.js
Normal file
48
web/source/settings-panel/lib/submit.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
48
web/source/settings-panel/redux/index.js
Normal file
48
web/source/settings-panel/redux/index.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 };
|
131
web/source/settings-panel/redux/reducers/admin.js
Normal file
131
web/source/settings-panel/redux/reducers/admin.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
42
web/source/settings-panel/redux/reducers/instances.js
Normal file
42
web/source/settings-panel/redux/reducers/instances.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
52
web/source/settings-panel/redux/reducers/oauth.js
Normal file
52
web/source/settings-panel/redux/reducers/oauth.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
32
web/source/settings-panel/redux/reducers/temporary.js
Normal file
32
web/source/settings-panel/redux/reducers/temporary.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {createSlice} = require("@reduxjs/toolkit");
|
||||||
|
|
||||||
|
module.exports = createSlice({
|
||||||
|
name: "temporary",
|
||||||
|
initialState: {
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
setStatus: function(state, {payload}) {
|
||||||
|
state.status = payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
51
web/source/settings-panel/redux/reducers/user.js
Normal file
51
web/source/settings-panel/redux/reducers/user.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
498
web/source/settings-panel/style.css
Normal file
498
web/source/settings-panel/style.css
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
113
web/source/settings-panel/user/profile.js
Normal file
113
web/source/settings-panel/user/profile.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
<div className="user-profile">
|
||||||
|
<h1>Profile</h1>
|
||||||
|
<div className="overview">
|
||||||
|
<div className="profile">
|
||||||
|
<div className="headerimage">
|
||||||
|
<img className="headerpreview" src={account.header} alt={account.header ? `header image for ${account.username}` : "None set"} />
|
||||||
|
</div>
|
||||||
|
<div className="basic">
|
||||||
|
<div id="profile-basic-filler2"></div>
|
||||||
|
<span className="avatar"><img className="avatarpreview" src={account.avatar} alt={account.avatar ? `avatar image for ${account.username}` : "None set"} /></span>
|
||||||
|
<div className="displayname">{account.display_name.trim().length > 0 ? account.display_name : account.username}</div>
|
||||||
|
<div className="username"><span>@{account.username}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="files">
|
||||||
|
<div>
|
||||||
|
<h3>Header</h3>
|
||||||
|
<File
|
||||||
|
id="header"
|
||||||
|
fileType="image/*"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Avatar</h3>
|
||||||
|
<File
|
||||||
|
id="avatar"
|
||||||
|
fileType="image/*"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TextInput
|
||||||
|
id="display_name"
|
||||||
|
name="Name"
|
||||||
|
placeHolder="A GoToSocial user"
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
id="source.note"
|
||||||
|
name="Bio"
|
||||||
|
placeHolder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
id="locked"
|
||||||
|
name="Manually approve follow requests? "
|
||||||
|
/>
|
||||||
|
{ !allowCustomCSS ? null :
|
||||||
|
<TextArea
|
||||||
|
id="custom_css"
|
||||||
|
name="Custom CSS"
|
||||||
|
className="monospace"
|
||||||
|
>
|
||||||
|
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom profile CSS (opens in a new tab)</a>
|
||||||
|
</TextArea>
|
||||||
|
}
|
||||||
|
<Submit onClick={saveProfile} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
140
web/source/settings-panel/user/settings.js
Normal file
140
web/source/settings-panel/user/settings.js
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"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 (
|
||||||
|
<>
|
||||||
|
<div className="user-settings">
|
||||||
|
<h1>Post settings</h1>
|
||||||
|
<Select id="source.language" name="Default post language" options={
|
||||||
|
<Languages/>
|
||||||
|
}>
|
||||||
|
</Select>
|
||||||
|
<Select id="source.privacy" name="Default post privacy" options={
|
||||||
|
<>
|
||||||
|
<option value="private">Private / followers-only</option>
|
||||||
|
<option value="unlisted">Unlisted</option>
|
||||||
|
<option value="public">Public</option>
|
||||||
|
</>
|
||||||
|
}>
|
||||||
|
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a>
|
||||||
|
</Select>
|
||||||
|
<Select id="source.status_format" name="Default post format" options={
|
||||||
|
<>
|
||||||
|
<option value="plain">Plain (default)</option>
|
||||||
|
<option value="markdown">Markdown</option>
|
||||||
|
</>
|
||||||
|
}>
|
||||||
|
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a>
|
||||||
|
</Select>
|
||||||
|
<Checkbox
|
||||||
|
id="source.sensitive"
|
||||||
|
name="Mark my posts as sensitive by default"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Submit onClick={updateSettings} label="Save post settings" errorMsg={errorMsg} statusMsg={statusMsg}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<PasswordChange/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function PasswordChange() {
|
||||||
|
const dispatch = Redux.useDispatch();
|
||||||
|
|
||||||
|
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("");
|
||||||
|
|
||||||
|
function changePassword() {
|
||||||
|
if (newPassword !== newPasswordConfirm) {
|
||||||
|
setError("New password and confirm new password did not match!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("PATCHing");
|
||||||
|
setError("");
|
||||||
|
return Promise.try(() => {
|
||||||
|
let data = {
|
||||||
|
old_password: oldPassword,
|
||||||
|
new_password: newPassword
|
||||||
|
};
|
||||||
|
return dispatch(api.apiCall("POST", "/api/v1/user/password_change", data, "form"));
|
||||||
|
}).then(() => {
|
||||||
|
setStatus("Saved!");
|
||||||
|
setOldPassword("");
|
||||||
|
setNewPassword("");
|
||||||
|
setNewPasswordConfirm("");
|
||||||
|
}).catch((e) => {
|
||||||
|
setError(e.message);
|
||||||
|
setStatus("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Change password</h1>
|
||||||
|
<div className="labelinput">
|
||||||
|
<label htmlFor="password">Current password</label>
|
||||||
|
<input name="password" id="password" type="password" autoComplete="current-password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="labelinput">
|
||||||
|
<label htmlFor="new-password">New password</label>
|
||||||
|
<input name="new-password" id="new-password" type="password" autoComplete="new-password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="labelinput">
|
||||||
|
<label htmlFor="confirm-new-password">Confirm new password</label>
|
||||||
|
<input name="confirm-new-password" id="confirm-new-password" type="password" autoComplete="new-password" value={newPasswordConfirm} onChange={(e) => setNewPasswordConfirm(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<Submit onClick={changePassword} label="Save new password" errorMsg={errorMsg} statusMsg={statusMsg}/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
1516
web/source/yarn.lock
1516
web/source/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,6 @@
|
||||||
{{ template "header.tmpl" .}}
|
{{ template "header.tmpl" .}}
|
||||||
<main class="lightgray">
|
<main class="lightgray">
|
||||||
<div id="root"></div>
|
<div id="root">
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{{ template "footer.tmpl" .}}
|
{{ template "footer.tmpl" .}}
|
|
@ -1,5 +1,5 @@
|
||||||
{{ template "header.tmpl" .}}
|
{{ template "header.tmpl" .}}
|
||||||
<section class="excerpt_top">
|
<section class="excerpt-top">
|
||||||
home to <span class="count">{{.instance.Stats.user_count}}</span> users
|
home to <span class="count">{{.instance.Stats.user_count}}</span> users
|
||||||
who posted <span class="count">{{.instance.Stats.status_count}}</span> statuses,
|
who posted <span class="count">{{.instance.Stats.status_count}}</span> statuses,
|
||||||
federating with <span class="count">{{.instance.Stats.domain_count}}</span> other instances.
|
federating with <span class="count">{{.instance.Stats.domain_count}}</span> other instances.
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
<a href="{{.Account.URL}}" class="avatar"><img src="{{.Account.Avatar}}" alt=""></a>
|
<a href="{{.Account.URL}}" class="avatar"><img src="{{.Account.Avatar}}" alt=""></a>
|
||||||
<a href="{{.Account.URL}}" class="displayname">{{if .Account.DisplayName}}{{emojify .Account.Emojis (escape .Account.DisplayName)}}{{else}}{{.Account.Username}}{{end}}</a>
|
<a href="{{.Account.URL}}" class="displayname">{{if .Account.DisplayName}}{{emojify .Account.Emojis (escape .Account.DisplayName)}}{{else}}{{.Account.Username}}{{end}}</a>
|
||||||
<a href="{{.Account.URL}}" class="username">@{{.Account.Acct}}</a>
|
<a href="{{.Account.URL}}" class="username">@{{.Account.Acct}}</a>
|
||||||
|
<div class="not-expanded">
|
||||||
|
<span class="visibility">{{.Visibility | visibilityIcon}}</span>
|
||||||
|
<span class="date">{{.CreatedAt | timestamp}}</span>
|
||||||
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
{{if .SpoilerText}}
|
{{if .SpoilerText}}
|
||||||
<input class="spoiler" id="hideSpoiler-{{.ID}}" type="checkbox" style="display: none" aria-hidden="true" checked="true" />
|
<input class="spoiler" id="hideSpoiler-{{.ID}}" type="checkbox" style="display: none" aria-hidden="true" checked="true" />
|
||||||
|
@ -43,7 +47,6 @@
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div id="date">{{.CreatedAt | timestamp}}</div>
|
<div id="date">{{.CreatedAt | timestamp}}</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div id="visibility">{{.Visibility | visibilityIcon}}</div>
|
|
||||||
<div id="replies"><i aria-label="Replies" class="fa fa-reply-all"></i> {{.RepliesCount}}</div>
|
<div id="replies"><i aria-label="Replies" class="fa fa-reply-all"></i> {{.RepliesCount}}</div>
|
||||||
<div id="boosts"><i aria-label="Boosts" class="fa fa-retweet"></i> {{.ReblogsCount}}</div>
|
<div id="boosts"><i aria-label="Boosts" class="fa fa-retweet"></i> {{.ReblogsCount}}</div>
|
||||||
<div id="favorites"><i aria-label="Favorites" class="fa fa-star"></i> {{.FavouritesCount}}</div>
|
<div id="favorites"><i aria-label="Favorites" class="fa fa-star"></i> {{.FavouritesCount}}</div>
|
||||||
|
|
Loading…
Reference in a new issue