mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 23:32:24 -06: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
					
				
			
		
					 59 changed files with 3989 additions and 2837 deletions
				
			
		| 
						 | 
					@ -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 @@ import (
 | 
				
			||||||
	"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 @@ const (
 | 
				
			||||||
	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 {
 | 
				
			||||||
 | 
								background: $button-danger-hover-bg;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	&:hover {
 | 
						&:hover {
 | 
				
			||||||
		background: $button_hover_bg;
 | 
							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 {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,19 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
   GoToSocial
 | 
						GoToSocial
 | 
				
			||||||
   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
						Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is free software: you can redistribute it and/or modify
 | 
						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
 | 
						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
 | 
						the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
   (at your option) any later version.
 | 
						(at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is distributed in the hope that it will be useful,
 | 
						This program is distributed in the hope that it will be useful,
 | 
				
			||||||
   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
   GNU Affero General Public License for more details.
 | 
						GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   You should have received a copy of the GNU Affero General Public License
 | 
						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/>.
 | 
						along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,19 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
   GoToSocial
 | 
						GoToSocial
 | 
				
			||||||
   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
						Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is free software: you can redistribute it and/or modify
 | 
						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
 | 
						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
 | 
						the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
   (at your option) any later version.
 | 
						(at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is distributed in the hope that it will be useful,
 | 
						This program is distributed in the hope that it will be useful,
 | 
				
			||||||
   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
   GNU Affero General Public License for more details.
 | 
						GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   You should have received a copy of the GNU Affero General Public License
 | 
						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/>.
 | 
						along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
| 
						 | 
					@ -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,19 +1,19 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
   GoToSocial
 | 
						GoToSocial
 | 
				
			||||||
   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
						Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is free software: you can redistribute it and/or modify
 | 
						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
 | 
						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
 | 
						the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
   (at your option) any later version.
 | 
						(at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is distributed in the hope that it will be useful,
 | 
						This program is distributed in the hope that it will be useful,
 | 
				
			||||||
   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
   GNU Affero General Public License for more details.
 | 
						GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   You should have received a copy of the GNU Affero General Public License
 | 
						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/>.
 | 
						along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,19 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
   GoToSocial
 | 
						GoToSocial
 | 
				
			||||||
   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
						Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is free software: you can redistribute it and/or modify
 | 
						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
 | 
						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
 | 
						the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
   (at your option) any later version.
 | 
						(at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is distributed in the hope that it will be useful,
 | 
						This program is distributed in the hope that it will be useful,
 | 
				
			||||||
   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
   GNU Affero General Public License for more details.
 | 
						GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   You should have received a copy of the GNU Affero General Public License
 | 
						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/>.
 | 
						along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
							
								
								
									
										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 };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,19 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
   GoToSocial
 | 
						GoToSocial
 | 
				
			||||||
   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
						Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is free software: you can redistribute it and/or modify
 | 
						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
 | 
						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
 | 
						the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
   (at your option) any later version.
 | 
						(at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   This program is distributed in the hope that it will be useful,
 | 
						This program is distributed in the hope that it will be useful,
 | 
				
			||||||
   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
   GNU Affero General Public License for more details.
 | 
						GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   You should have received a copy of the GNU Affero General Public License
 | 
						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/>.
 | 
						along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue