mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 06:42:25 -05: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 { | 		&:hover { | ||||||
| 		background: $button_hover_bg; | 			background: $button-danger-hover-bg; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&:hover { | ||||||
|  | 		background: $button-hover-bg; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -191,7 +204,7 @@ section.apps { | ||||||
| 			grid-template-columns: 25% 1fr; | 			grid-template-columns: 25% 1fr; | ||||||
| 			gap: 1.5rem; | 			gap: 1.5rem; | ||||||
| 			padding: 0.5rem; | 			padding: 0.5rem; | ||||||
| 			background: $bg_accent; | 			background: $bg-accent; | ||||||
| 			border-radius: 0.5rem; | 			border-radius: 0.5rem; | ||||||
| 
 | 
 | ||||||
| 			.logo { | 			.logo { | ||||||
|  | @ -211,7 +224,7 @@ section.apps { | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			div { | 			div { | ||||||
| 				padding: 1rem 0; | 				padding: 0; | ||||||
| 				h3 { | 				h3 { | ||||||
| 					margin-top: 0; | 					margin-top: 0; | ||||||
| 				} | 				} | ||||||
|  | @ -264,26 +277,42 @@ section.error { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .error-text { | ||||||
|  | 	color: $error1; | ||||||
|  | 	background: $error2; | ||||||
|  | 	border-radius: 0.1rem; | ||||||
|  | 	font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| input, select, textarea { | input, select, textarea { | ||||||
| 	box-sizing: border-box; | 	box-sizing: border-box; | ||||||
| 	border: 0.15rem solid $border_accent; | 	border: 0.15rem solid $input-border; | ||||||
| 	border-radius: 0.1rem; | 	border-radius: 0.1rem; | ||||||
| 	color: $fg; | 	color: $fg; | ||||||
| 	/* background: $input_bg; */ | 	background: $input-bg; | ||||||
| 	background: $bg_accent; |  | ||||||
| 	width: 100%; | 	width: 100%; | ||||||
| 	font-family: 'Noto Sans', sans-serif; | 	font-family: 'Noto Sans', sans-serif; | ||||||
| 	font-size: 1rem; | 	font-size: 1rem; | ||||||
| 	padding: 0.3rem; | 	padding: 0.3rem; | ||||||
| 
 | 
 | ||||||
| 	&:focus { | 	&:focus { | ||||||
| 		border-color: $fg_accent; | 		border-color: $input-focus-border; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&:disabled { | ||||||
|  | 		background: $input-disabled-bg; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| input, textarea { | ::placeholder { | ||||||
| 	padding-top: 0.1rem; | 	opacity: 1; | ||||||
| 	padding-bottom: 0.1rem; | 	color: $fg-reduced | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | hr { | ||||||
|  | 	color: transparent; | ||||||
|  | 	width: 100%; | ||||||
|  | 	border-bottom: 0.02rem solid $border-accent; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| footer { | footer { | ||||||
|  | @ -331,3 +360,7 @@ footer { | ||||||
| 	object-fit: contain; | 	object-fit: contain; | ||||||
| 	vertical-align: middle; | 	vertical-align: middle; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .monospace { | ||||||
|  | 	font-family: monospace; | ||||||
|  | } | ||||||
|  | @ -28,7 +28,7 @@ main { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .profile { | .profile { | ||||||
| 	background: $bg_accent; | 	background: $bg-accent; | ||||||
| 	display: grid; | 	display: grid; | ||||||
| 	grid-template-rows: auto auto auto; | 	grid-template-rows: auto auto auto; | ||||||
| 	grid-template-columns: auto; | 	grid-template-columns: auto; | ||||||
|  | @ -38,7 +38,7 @@ main { | ||||||
| 
 | 
 | ||||||
| 	border-radius: $br; | 	border-radius: $br; | ||||||
| 	box-shadow: $boxshadow; | 	box-shadow: $boxshadow; | ||||||
| 	border: $boxshadow_border; | 	border: $boxshadow-border; | ||||||
| 
 | 
 | ||||||
| 	.headerimage { | 	.headerimage { | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
|  | @ -50,7 +50,7 @@ main { | ||||||
| 			width: 100%; | 			width: 100%; | ||||||
| 			height: 100%; | 			height: 100%; | ||||||
| 			object-fit: cover; | 			object-fit: cover; | ||||||
| 			border-radius: $br_inner $br_inner 0 0; | 			border-radius: $br-inner $br-inner 0 0; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -69,7 +69,7 @@ main { | ||||||
| 
 | 
 | ||||||
| 		#profile-basic-filler2 { | 		#profile-basic-filler2 { | ||||||
| 			grid-area: filler2; | 			grid-area: filler2; | ||||||
| 			background: $bg_trans; | 			background: $bg-trans; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		.avatar { | 		.avatar { | ||||||
|  | @ -79,7 +79,7 @@ main { | ||||||
| 			width: 8.5rem; | 			width: 8.5rem; | ||||||
| 			grid-area: avatar; | 			grid-area: avatar; | ||||||
| 			background: $bg; | 			background: $bg; | ||||||
| 			border: $profile_avatar_border; | 			border: 0.2rem solid $avatar-border; | ||||||
| 			padding: 0; | 			padding: 0; | ||||||
| 			border-radius: $br; | 			border-radius: $br; | ||||||
| 			position: relative; | 			position: relative; | ||||||
|  | @ -87,7 +87,7 @@ main { | ||||||
| 			box-shadow: $boxshadow; | 			box-shadow: $boxshadow; | ||||||
| 			img { | 			img { | ||||||
| 				object-fit: cover; | 				object-fit: cover; | ||||||
| 				border-radius: $br_inner; | 				border-radius: $br-inner; | ||||||
| 				width: 100%; | 				width: 100%; | ||||||
| 				height: 100%; | 				height: 100%; | ||||||
| 			} | 			} | ||||||
|  | @ -105,7 +105,7 @@ main { | ||||||
| 			font-weight: bold; | 			font-weight: bold; | ||||||
| 			font-size: 2rem; | 			font-size: 2rem; | ||||||
| 			line-height: 2.2rem; | 			line-height: 2.2rem; | ||||||
| 			background: $bg_trans; | 			background: $bg-trans; | ||||||
| 			word-break: break-all; | 			word-break: break-all; | ||||||
| 			text-overflow: ellipsis; | 			text-overflow: ellipsis; | ||||||
| 			overflow: hidden; | 			overflow: hidden; | ||||||
|  | @ -120,7 +120,7 @@ main { | ||||||
| 			padding-top: 0; | 			padding-top: 0; | ||||||
| 			margin-top: 0.25rem; | 			margin-top: 0.25rem; | ||||||
| 			padding-bottom: 0.25rem; | 			padding-bottom: 0.25rem; | ||||||
| 			color: $fg_accent; | 			color: $fg-accent; | ||||||
| 			font-weight: bold; | 			font-weight: bold; | ||||||
| 			word-break: break-all; | 			word-break: break-all; | ||||||
| 			text-overflow: ellipsis; | 			text-overflow: ellipsis; | ||||||
|  |  | ||||||
|  | @ -31,13 +31,13 @@ main { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .toot { | .toot { | ||||||
| 	background: $status_unfocus_bg; | 	background: $toot-unfocus-bg; | ||||||
| 	box-shadow: $boxshadow; | 	box-shadow: $boxshadow; | ||||||
| 	border: $boxshadow_border; | 	border: $boxshadow-border; | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	margin-bottom: $br; | 	margin-bottom: $br; | ||||||
| 	border-radius: $br; | 	padding-top: 1.5rem; | ||||||
| 	padding: 1.5rem 0; | 	padding-bottom: 0.7rem; | ||||||
| 
 | 
 | ||||||
| 	a { | 	a { | ||||||
| 		position: relative; | 		position: relative; | ||||||
|  | @ -49,27 +49,34 @@ main { | ||||||
| 	.contentgrid { | 	.contentgrid { | ||||||
| 		padding: 0 1.5rem; | 		padding: 0 1.5rem; | ||||||
| 		display: grid; | 		display: grid; | ||||||
| 		grid-template-columns: 4rem auto 1fr; | 		grid-template-columns: 4rem 1fr auto; | ||||||
| 		grid-template-rows: 1.5rem auto auto; | 		grid-template-rows: 1.5rem auto auto auto; | ||||||
| 		column-gap: 0.5rem; | 		column-gap: 0.5rem; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	.not-expanded { | ||||||
|  | 		color: $fg-reduced; | ||||||
|  | 		grid-column: 3; | ||||||
|  | 		grid-row: 1; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	.avatar { | 	.avatar { | ||||||
| 		grid-row: span 2; | 		grid-row: span 3; | ||||||
| 		aspect-ratio: 1/1; | 		aspect-ratio: 1/1; | ||||||
|  | 		display: flex; | ||||||
|  | 		border: 0.2rem solid $avatar-border; | ||||||
|  | 		border-radius: 0.4rem; | ||||||
|  | 		overflow: hidden; /* hides corners from img overflowing */ | ||||||
| 	 | 	 | ||||||
| 		img { | 		img { | ||||||
| 			height: 100%; | 			height: 100%; | ||||||
| 			width: 100%; | 			width: 100%; | ||||||
| 			object-fit: cover; | 			object-fit: cover; | ||||||
| 			background: $bg; | 			background: $bg; | ||||||
| 			border: 0.1rem solid $acc2; |  | ||||||
| 			border-radius: calc($br / 1.5); |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	.displayname { | 	.displayname { | ||||||
| 		grid-column: span 2; |  | ||||||
| 		font-weight: bold; | 		font-weight: bold; | ||||||
| 		font-size: 1.2rem; | 		font-size: 1.2rem; | ||||||
| 		line-height: 2rem; | 		line-height: 2rem; | ||||||
|  | @ -82,7 +89,7 @@ main { | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	.username { | 	.username { | ||||||
| 		color: $link_fg; | 		color: $link-fg; | ||||||
| 		line-height: 2rem; | 		line-height: 2rem; | ||||||
| 		margin-top: -0.5rem; | 		margin-top: -0.5rem; | ||||||
| 		align-self: start; | 		align-self: start; | ||||||
|  | @ -119,8 +126,7 @@ main { | ||||||
| 
 | 
 | ||||||
| 	.text { | 	.text { | ||||||
| 		margin: 0; | 		margin: 0; | ||||||
| 		margin-top: 0.5rem; | 		grid-column: 2 / span 2; | ||||||
| 		grid-column: span 3; |  | ||||||
| 		grid-row: span 1; | 		grid-row: span 1; | ||||||
| 		overflow: hidden; | 		overflow: hidden; | ||||||
| 
 | 
 | ||||||
|  | @ -128,34 +134,33 @@ main { | ||||||
| 		z-index: 2; | 		z-index: 2; | ||||||
| 
 | 
 | ||||||
| 		a { | 		a { | ||||||
| 			color: $link_fg; | 			color: $link-fg; | ||||||
| 			text-decoration: underline; | 			text-decoration: underline; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		.content { | 		.content { | ||||||
| 			padding-top: 0.5rem; |  | ||||||
| 			padding-bottom: 0.5rem; | 			padding-bottom: 0.5rem; | ||||||
| 
 | 
 | ||||||
| 			word-break: break-word; | 			word-break: break-word; | ||||||
| 
 | 
 | ||||||
| 			blockquote { | 			blockquote { | ||||||
| 				padding: 0.5rem 0 0.5rem 1.5rem; | 				padding: 0.5rem 0 0.5rem 1.5rem; | ||||||
| 				border-left: 0.2rem solid $sloth_orange1; | 				border-left: 0.2rem solid $border-accent; | ||||||
| 				margin-left: 1rem; | 				margin-left: 1rem; | ||||||
| 				font-style: italic; | 				font-style: italic; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			hr { | 			hr { | ||||||
| 				border: 1px dashed $sloth_orange1; | 				border: 1px dashed $border-accent; | ||||||
| 			}  | 			}  | ||||||
| 
 | 
 | ||||||
| 			pre, code { | 			pre, code { | ||||||
| 				background-color: $sloth_gray2_darker7; | 				background-color: $gray2; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			code { | 			code { | ||||||
| 				padding: 0.25rem; | 				padding: 0.25rem; | ||||||
| 				border-radius: $br_inner; | 				border-radius: $br-inner; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			pre { | 			pre { | ||||||
|  | @ -249,7 +254,7 @@ main { | ||||||
| 
 | 
 | ||||||
| 			.closed { | 			.closed { | ||||||
| 				transition: 0.3s; | 				transition: 0.3s; | ||||||
| 				background: $bg_sensitive; | 				background: $bg-sensitive; | ||||||
| 				@supports (backdrop-filter: blur(2rem)) { | 				@supports (backdrop-filter: blur(2rem)) { | ||||||
| 					background: transparent; | 					background: transparent; | ||||||
| 					backdrop-filter: blur(2rem); | 					backdrop-filter: blur(2rem); | ||||||
|  | @ -263,17 +268,17 @@ main { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		.no-image-desc { | 		.no-image-desc { | ||||||
| 			color: $button_fg; | 			color: $no-img-desc-fg; | ||||||
|  | 			background: $no-img-desc-bg; | ||||||
| 			display: flex; | 			display: flex; | ||||||
| 			position: absolute; | 			position: absolute; | ||||||
| 			bottom: 0.1rem; | 			bottom: 0.1rem; | ||||||
| 			right: 0.4rem; | 			right: 0.4rem; | ||||||
| 			margin-bottom: 0.4rem; | 			margin-bottom: 0.4rem; | ||||||
| 			margin-right: 0.4rem; | 			margin-right: 0.4rem; | ||||||
| 			background: $bg_no_img_desc; |  | ||||||
| 			padding: 0.1rem 0.45rem; | 			padding: 0.1rem 0.45rem; | ||||||
| 			border-radius: 100%; | 			border-radius: 100%; | ||||||
| 			border: 0.2rem solid $button_fg; | 			border: 0.2rem solid $button-fg; | ||||||
| 			z-index: 3; | 			z-index: 3; | ||||||
| 
 | 
 | ||||||
| 			i.fa { | 			i.fa { | ||||||
|  | @ -302,12 +307,13 @@ main { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	.info { | 	.info { | ||||||
|  | 		background: $toot-info-bg; | ||||||
|  | 		color: $fg-reduced; | ||||||
| 		display: none; | 		display: none; | ||||||
| 		border-top: 0.15rem solid $status_unfocus_bg; | 		border-top: 0.15rem solid $toot-info-border; | ||||||
| 		padding: 0.5rem 1.5rem; | 		padding: 0.5rem 1.5rem; | ||||||
| 
 | 
 | ||||||
| 		div { | 		div { | ||||||
| 			position: relative; |  | ||||||
| 			padding-right: 1.3rem; | 			padding-right: 1.3rem; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -317,30 +323,6 @@ main { | ||||||
| 
 | 
 | ||||||
| 		grid-column: span 3; | 		grid-column: span 3; | ||||||
| 		flex-wrap: wrap; | 		flex-wrap: wrap; | ||||||
| 
 |  | ||||||
| 		div.stats::after { |  | ||||||
| 			display: none; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		div::after { |  | ||||||
| 			$size: 0.25rem; |  | ||||||
| 			display: block; |  | ||||||
| 			background: $fg_dark; |  | ||||||
| 			height: $size; |  | ||||||
| 			width: $size; |  | ||||||
| 			content: ""; |  | ||||||
| 			position: absolute; |  | ||||||
| 			top: calc((1.5rem - $size) / 2); |  | ||||||
| 			right: 0.55rem; |  | ||||||
| 			border-radius: 1rem; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		div:last-child { |  | ||||||
| 			&::after { |  | ||||||
| 				display: none; |  | ||||||
| 			} |  | ||||||
| 			margin-right: 0; |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	.toot-link { | 	.toot-link { | ||||||
|  | @ -362,7 +344,7 @@ main { | ||||||
| 		border-top-right-radius: $br; | 		border-top-right-radius: $br; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&:last-child { | 	&:last-child, &:last-child .info { | ||||||
| 		/* bottom left, bottom right */ | 		/* bottom left, bottom right */ | ||||||
| 		border-bottom-left-radius: $br; | 		border-bottom-left-radius: $br; | ||||||
| 		border-bottom-right-radius: $br; | 		border-bottom-right-radius: $br; | ||||||
|  | @ -370,11 +352,21 @@ main { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&.expanded { | 	&.expanded { | ||||||
| 		background: $status_focus_bg; | 		background: $toot-focus-bg; | ||||||
| 		padding-bottom: 0; | 		padding-bottom: 0; | ||||||
| 
 | 
 | ||||||
| 		.contentgrid { | 		.contentgrid { | ||||||
| 			padding-bottom: 1rem; | 			.displayname { | ||||||
|  | 				grid-column: span 2; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			.text { | ||||||
|  | 				grid-column: 1 / span 3; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			.not-expanded { | ||||||
|  | 				display: none; | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		.info { | 		.info { | ||||||
|  |  | ||||||
|  | @ -18,11 +18,6 @@ | ||||||
| 
 | 
 | ||||||
| "use strict"; | "use strict"; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| // WARNING: currently dependencies get deduplicated with factor-bundle, but 
 |  | ||||||
| // our frontend templates don't load the common bundle.js since it contains React etc
 |  | ||||||
| // so we can't use any dependencies that would deduplicate with the other files
 |  | ||||||
| 
 |  | ||||||
| const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js"); | const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js"); | ||||||
| const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js"); | const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js"); | ||||||
| const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default; | const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default; | ||||||
|  |  | ||||||
|  | @ -23,7 +23,8 @@ | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
| const budoExpress = require('budo-express'); | // Forked budo-express supports EventEmitter, to write bundle.js to disk in development
 | ||||||
|  | const budoExpress = require('@f0x52/budo-express'); | ||||||
| const babelify = require('babelify'); | const babelify = require('babelify'); | ||||||
| const fs = require("fs"); | const fs = require("fs"); | ||||||
| const EventEmitter = require('events'); | const EventEmitter = require('events'); | ||||||
|  | @ -38,8 +39,9 @@ const splitCSS = require("./lib/split-css.js"); | ||||||
| 
 | 
 | ||||||
| const bundles = { | const bundles = { | ||||||
| 	"./frontend/index.js": "frontend.js", | 	"./frontend/index.js": "frontend.js", | ||||||
| 	"./panels/admin/index.js": "admin-panel.js", | 	"./settings-panel/index.js": "settings.js", | ||||||
| 	"./panels/user/index.js": "user-panel.js", | 	// "./panels/admin/index.js": "admin-panel.js",
 | ||||||
|  | 	// "./panels/user/index.js": "user-panel.js",
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const postcssPlugins = [ | const postcssPlugins = [ | ||||||
|  | @ -50,6 +52,18 @@ const postcssPlugins = [ | ||||||
| 	"postcss-color-mod-function" | 	"postcss-color-mod-function" | ||||||
| ].map((plugin) => require(plugin)()); | ].map((plugin) => require(plugin)()); | ||||||
| 
 | 
 | ||||||
|  | let uglifyifyInProduction; | ||||||
|  | 
 | ||||||
|  | if (process.env.NODE_ENV != "development") { | ||||||
|  | 	console.log("uglifyify'ing production bundles"); | ||||||
|  | 	uglifyifyInProduction = [ | ||||||
|  | 		require("uglifyify"), { | ||||||
|  | 			global: true, | ||||||
|  | 			exts: ".js" | ||||||
|  | 		} | ||||||
|  | 	]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| const browserifyConfig = { | const browserifyConfig = { | ||||||
| 	transform: [ | 	transform: [ | ||||||
| 		[ | 		[ | ||||||
|  | @ -69,10 +83,7 @@ const browserifyConfig = { | ||||||
| 				exclude: /node_modules\/(?!photoswipe-dynamic-caption-plugin)/, | 				exclude: /node_modules\/(?!photoswipe-dynamic-caption-plugin)/, | ||||||
| 			} | 			} | ||||||
| 		], | 		], | ||||||
| 		[require("uglifyify"), { | 		uglifyifyInProduction | ||||||
| 			global: true, |  | ||||||
| 			exts: ".js" |  | ||||||
| 		}] |  | ||||||
| 	], | 	], | ||||||
| 	plugin: [ | 	plugin: [ | ||||||
| 		[require("icssify"), { | 		[require("icssify"), { | ||||||
|  | @ -86,7 +97,8 @@ const browserifyConfig = { | ||||||
| 				return out(file); | 				return out(file); | ||||||
| 			}) | 			}) | ||||||
| 		}] | 		}] | ||||||
| 	] | 	], | ||||||
|  | 	extensions: [".js", ".jsx", ".css"] | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const entryFiles = Object.keys(bundles); | const entryFiles = Object.keys(bundles); | ||||||
|  |  | ||||||
|  | @ -1,30 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const React = require("react"); |  | ||||||
| 
 |  | ||||||
| module.exports = function Submit({onClick, label, errorMsg, statusMsg}) { |  | ||||||
| 	return ( |  | ||||||
| 		<div className="messagebutton"> |  | ||||||
| 			<button type="submit" onClick={onClick}>{ label }</button> |  | ||||||
| 			<div className="error accent">{errorMsg ? errorMsg : statusMsg}</div> |  | ||||||
| 		</div> |  | ||||||
| 	); |  | ||||||
| }; |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "gotosocial-frontend", |   "name": "gotosocial-frontend", | ||||||
|   "version": "0.3.8", |   "version": "0.5.0", | ||||||
|   "description": "GoToSocial frontend sources", |   "description": "GoToSocial frontend sources", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "author": "f0x", |   "author": "f0x", | ||||||
|  | @ -9,18 +9,23 @@ | ||||||
|     "@babel/core": "^7.12.13", |     "@babel/core": "^7.12.13", | ||||||
|     "@babel/preset-env": "^7.12.13", |     "@babel/preset-env": "^7.12.13", | ||||||
|     "@babel/preset-react": "^7.12.13", |     "@babel/preset-react": "^7.12.13", | ||||||
|  |     "@f0x52/budo-express": "^1.1.0", | ||||||
|  |     "@reduxjs/toolkit": "^1.8.5", | ||||||
|     "autoprefixer": "^10.4.8", |     "autoprefixer": "^10.4.8", | ||||||
|     "babelify": "^10.0.0", |     "babelify": "^10.0.0", | ||||||
|     "bluebird": "^3.7.2", |     "bluebird": "^3.7.2", | ||||||
|     "browserify": "^17.0.0", |     "browserify": "^17.0.0", | ||||||
|     "browserlist": "^1.0.1", |     "browserlist": "^1.0.1", | ||||||
|     "budo-express": "^1.0.8", |     "create-error": "^0.3.1", | ||||||
|     "css-extract": "^2.0.0", |     "css-extract": "^2.0.0", | ||||||
|  |     "default-value": "^1.0.0", | ||||||
|  |     "dotty": "^0.1.2", | ||||||
|     "eslint-plugin-react": "^7.24.0", |     "eslint-plugin-react": "^7.24.0", | ||||||
|     "express": "^4.18.1", |     "express": "^4.18.1", | ||||||
|     "factor-bundle": "^2.5.0", |     "factor-bundle": "^2.5.0", | ||||||
|     "from2-string": "^1.1.0", |  | ||||||
|     "icssify": "^2.0.0", |     "icssify": "^2.0.0", | ||||||
|  |     "is-plain-object": "^5.0.0", | ||||||
|  |     "is-valid-domain": "^0.1.6", | ||||||
|     "js-file-download": "^0.4.12", |     "js-file-download": "^0.4.12", | ||||||
|     "modern-normalize": "^1.1.0", |     "modern-normalize": "^1.1.0", | ||||||
|     "photoswipe": "^5.3.0", |     "photoswipe": "^5.3.0", | ||||||
|  | @ -31,11 +36,17 @@ | ||||||
|     "postcss-nested": "^5.0.6", |     "postcss-nested": "^5.0.6", | ||||||
|     "postcss-scss": "^4.0.4", |     "postcss-scss": "^4.0.4", | ||||||
|     "postcss-strip-inline-comments": "^0.1.5", |     "postcss-strip-inline-comments": "^0.1.5", | ||||||
|     "pretty-bytes": "^5.6.0", |     "prettier-bytes": "^1.0.4", | ||||||
|     "react": "^17.0.1", |     "pretty-bytes": "4", | ||||||
|     "react-dom": "^17.0.1", |     "react": "18", | ||||||
|     "reactify": "^1.1.1", |     "react-dom": "18", | ||||||
|     "uglifyify": "^5.0.2" |     "react-error-boundary": "^3.1.4", | ||||||
|  |     "react-redux": "^8.0.2", | ||||||
|  |     "redux-devtools-extension": "^2.13.9", | ||||||
|  |     "redux-persist": "^6.0.0", | ||||||
|  |     "redux-thunk": "^2.4.1", | ||||||
|  |     "uglifyify": "^5.0.2", | ||||||
|  |     "wouter": "^2.8.0-alpha.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@f0x52/eslint-config-react": "^1.1.0", |     "@f0x52/eslint-config-react": "^1.1.0", | ||||||
|  |  | ||||||
|  | @ -1,21 +0,0 @@ | ||||||
| # GoToSocial Admin Panel |  | ||||||
| 
 |  | ||||||
| Standalone web admin panel for [GoToSocial](https://github.com/superseriousbusiness/gotosocial). |  | ||||||
| 
 |  | ||||||
| A public hosted instance is also available at https://gts.superseriousbusiness.org/admin/, so you can fill your own instance URL in there. |  | ||||||
| 
 |  | ||||||
| ## Installation |  | ||||||
| Build requirements: some version of Node.js with npm, |  | ||||||
| ``` |  | ||||||
| git clone https://github.com/superseriousbusiness/gotosocial-admin.git && cd gotosocial-admin |  | ||||||
| npm install |  | ||||||
| node index.js |  | ||||||
| ``` |  | ||||||
| All processed build output will now be in `public/`, which you can copy over to a folder in your GoToSocial installation like `web/assets/admin`, or serve elsewhere. |  | ||||||
| No further configuration is required, authentication happens through normal OAUTH flow. |  | ||||||
| 
 |  | ||||||
| ## Development |  | ||||||
| Follow the installation steps, but run `NODE_ENV=development node index.js` to start the livereloading dev server instead. |  | ||||||
| 
 |  | ||||||
| ## License, donations |  | ||||||
| [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html). If you want to support my work, you can: <a href="https://liberapay.com/f0x/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a> |  | ||||||
|  | @ -1,318 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const Promise = require("bluebird"); |  | ||||||
| const React = require("react"); |  | ||||||
| const fileDownload = require("js-file-download"); |  | ||||||
| 
 |  | ||||||
| function sortBlocks(blocks) { |  | ||||||
| 	return blocks.sort((a, b) => { // alphabetical sort
 |  | ||||||
| 		return a.domain.localeCompare(b.domain); |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function deduplicateBlocks(blocks) { |  | ||||||
| 	let a = new Map(); |  | ||||||
| 	blocks.forEach((block) => { |  | ||||||
| 		a.set(block.id, block); |  | ||||||
| 	}); |  | ||||||
| 	return Array.from(a.values()); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = function Blocks({oauth}) { |  | ||||||
| 	const [blocks, setBlocks] = React.useState([]); |  | ||||||
| 	const [info, setInfo] = React.useState("Fetching blocks"); |  | ||||||
| 	const [errorMsg, setError] = React.useState(""); |  | ||||||
| 	const [checked, setChecked] = React.useState(new Set()); |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { |  | ||||||
| 		Promise.try(() => { |  | ||||||
| 			return oauth.apiRequest("/api/v1/admin/domain_blocks", undefined, undefined, "GET"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			setInfo(""); |  | ||||||
| 			setError(""); |  | ||||||
| 			setBlocks(sortBlocks(json)); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			setError(e.message); |  | ||||||
| 			setInfo(""); |  | ||||||
| 		}); |  | ||||||
| 	}, []); |  | ||||||
| 
 |  | ||||||
| 	let blockList = blocks.map((block) => { |  | ||||||
| 		function update(e) { |  | ||||||
| 			let newChecked = new Set(checked.values()); |  | ||||||
| 			if (e.target.checked) { |  | ||||||
| 				newChecked.add(block.id); |  | ||||||
| 			} else { |  | ||||||
| 				newChecked.delete(block.id); |  | ||||||
| 			} |  | ||||||
| 			setChecked(newChecked); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return ( |  | ||||||
| 			<React.Fragment key={block.id}> |  | ||||||
| 				<div><input type="checkbox" onChange={update} checked={checked.has(block.id)}></input></div> |  | ||||||
| 				<div>{block.domain}</div> |  | ||||||
| 				<div>{(new Date(block.created_at)).toLocaleString()}</div> |  | ||||||
| 			</React.Fragment> |  | ||||||
| 		); |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	function clearChecked() { |  | ||||||
| 		setChecked(new Set()); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function undoChecked() { |  | ||||||
| 		let amount = checked.size; |  | ||||||
| 		if(confirm(`Are you sure you want to remove ${amount} block(s)?`)) { |  | ||||||
| 			setInfo(""); |  | ||||||
| 			Promise.map(Array.from(checked.values()), (block) => { |  | ||||||
| 				console.log("deleting", block); |  | ||||||
| 				return oauth.apiRequest(`/api/v1/admin/domain_blocks/${block}`, "DELETE"); |  | ||||||
| 			}).then((res) => { |  | ||||||
| 				console.log(res); |  | ||||||
| 				setInfo(`Deleted ${amount} blocks: ${res.map((a) => a.domain).join(", ")}`); |  | ||||||
| 			}).catch((e) => { |  | ||||||
| 				setError(e); |  | ||||||
| 			}); |  | ||||||
| 
 |  | ||||||
| 			let newBlocks = blocks.filter((block) => { |  | ||||||
| 				if (checked.size > 0 && checked.has(block.id)) { |  | ||||||
| 					checked.delete(block.id); |  | ||||||
| 					return false; |  | ||||||
| 				} else { |  | ||||||
| 					return true; |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
| 			setBlocks(newBlocks); |  | ||||||
| 			clearChecked(); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<section className="blocks"> |  | ||||||
| 			<h1>Blocks</h1> |  | ||||||
| 			<div className="error accent">{errorMsg}</div> |  | ||||||
| 			<div>{info}</div> |  | ||||||
| 			<AddBlock oauth={oauth} blocks={blocks} setBlocks={setBlocks} /> |  | ||||||
| 			<h3>Blocks:</h3> |  | ||||||
| 			<div style={{display: "grid", gridTemplateColumns: "1fr auto"}}> |  | ||||||
| 				<span onClick={clearChecked} className="accent" style={{alignSelf: "end"}}>uncheck all</span> |  | ||||||
| 				<button onClick={undoChecked}>Unblock selected</button> |  | ||||||
| 			</div> |  | ||||||
| 			<div className="blocklist overflow"> |  | ||||||
| 				{blockList} |  | ||||||
| 			</div> |  | ||||||
| 			<BulkBlocking oauth={oauth} blocks={blocks} setBlocks={setBlocks}/> |  | ||||||
| 		</section> |  | ||||||
| 	); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function BulkBlocking({oauth, blocks, setBlocks}) { |  | ||||||
| 	const [bulk, setBulk] = React.useState(""); |  | ||||||
| 	const [blockMap, setBlockMap] = React.useState(new Map()); |  | ||||||
| 	const [output, setOutput] = React.useState(); |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { |  | ||||||
| 		let newBlockMap = new Map(); |  | ||||||
| 		blocks.forEach((block) => { |  | ||||||
| 			newBlockMap.set(block.domain, block); |  | ||||||
| 		}); |  | ||||||
| 		setBlockMap(newBlockMap); |  | ||||||
| 	}, [blocks]); |  | ||||||
| 
 |  | ||||||
| 	const fileRef = React.useRef(); |  | ||||||
| 
 |  | ||||||
| 	function error(e) { |  | ||||||
| 		setOutput(<div className="error accent">{e}</div>); |  | ||||||
| 		throw e; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function fileUpload() { |  | ||||||
| 		let reader = new FileReader(); |  | ||||||
| 		reader.addEventListener("load", (e) => { |  | ||||||
| 			try { |  | ||||||
| 				// TODO: use validatem?
 |  | ||||||
| 				let json = JSON.parse(e.target.result); |  | ||||||
| 				json.forEach((block) => { |  | ||||||
| 					console.log("block:", block); |  | ||||||
| 				}); |  | ||||||
| 			} catch(e) { |  | ||||||
| 				error(e.message); |  | ||||||
| 			} |  | ||||||
| 		}); |  | ||||||
| 		reader.readAsText(fileRef.current.files[0]); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { |  | ||||||
| 		if (fileRef && fileRef.current) { |  | ||||||
| 			fileRef.current.addEventListener("change", fileUpload); |  | ||||||
| 		} |  | ||||||
| 		return function cleanup() { |  | ||||||
| 			fileRef.current.removeEventListener("change", fileUpload); |  | ||||||
| 		}; |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	function textImport() { |  | ||||||
| 		Promise.try(() => { |  | ||||||
| 			if (bulk[0] == "[") { |  | ||||||
| 				// assume it's json
 |  | ||||||
| 				return JSON.parse(bulk); |  | ||||||
| 			} else { |  | ||||||
| 				return bulk.split("\n").map((val) => { |  | ||||||
| 					return { |  | ||||||
| 						domain: val.trim() |  | ||||||
| 					}; |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 		}).then((domains) => { |  | ||||||
| 			console.log(domains); |  | ||||||
| 			let before = domains.length; |  | ||||||
| 			setOutput(`Importing ${before} domain(s)`); |  | ||||||
| 			domains = domains.filter(({domain}) => { |  | ||||||
| 				return (domain != "" && !blockMap.has(domain)); |  | ||||||
| 			}); |  | ||||||
| 			setOutput(<span>{output}<br/>{`Deduplicated ${before - domains.length}/${before} with existing blocks, adding ${domains.length} block(s)`}</span>); |  | ||||||
| 			if (domains.length > 0) { |  | ||||||
| 				let data = new FormData(); |  | ||||||
| 				data.append("domains", new Blob([JSON.stringify(domains)], {type: "application/json"}), "import.json"); |  | ||||||
| 				return oauth.apiRequest("/api/v1/admin/domain_blocks?import=true", "POST", data, "form"); |  | ||||||
| 			} |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			console.log("bulk import result:", json); |  | ||||||
| 			setBlocks(sortBlocks(deduplicateBlocks([...json, ...blocks]))); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			error(e.message); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function textExport() { |  | ||||||
| 		setBulk(blocks.reduce((str, val) => { |  | ||||||
| 			if (typeof str == "object") { |  | ||||||
| 				return str.domain; |  | ||||||
| 			} else { |  | ||||||
| 				return str + "\n" + val.domain; |  | ||||||
| 			} |  | ||||||
| 		})); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function jsonExport() { |  | ||||||
| 		Promise.try(() => { |  | ||||||
| 			return oauth.apiRequest("/api/v1/admin/domain_blocks?export=true", "GET"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			fileDownload(JSON.stringify(json), "block-export.json"); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			error(e); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function textAreaUpdate(e) { |  | ||||||
| 		setBulk(e.target.value); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<React.Fragment> |  | ||||||
| 			<h3>Bulk import/export</h3> |  | ||||||
| 			<label htmlFor="bulk">Domains, one per line:</label> |  | ||||||
| 			<textarea value={bulk} rows={20} onChange={textAreaUpdate}></textarea> |  | ||||||
| 			<div className="controls"> |  | ||||||
| 				<button onClick={textImport}>Import All From Field</button> |  | ||||||
| 				<button onClick={textExport}>Export To Field</button> |  | ||||||
| 				<label className="button" htmlFor="upload">Upload .json</label> |  | ||||||
| 				<button onClick={jsonExport}>Download .json</button> |  | ||||||
| 			</div> |  | ||||||
| 			{output} |  | ||||||
| 			<input type="file" id="upload" className="hidden" ref={fileRef}></input> |  | ||||||
| 		</React.Fragment> |  | ||||||
| 	); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function AddBlock({oauth, blocks, setBlocks}) { |  | ||||||
| 	const [domain, setDomain] = React.useState(""); |  | ||||||
| 	const [type, setType] = React.useState("suspend"); |  | ||||||
| 	const [obfuscated, setObfuscated] = React.useState(false); |  | ||||||
| 	const [privateDescription, setPrivateDescription] = React.useState(""); |  | ||||||
| 	const [publicDescription, setPublicDescription] = React.useState(""); |  | ||||||
| 
 |  | ||||||
| 	function addBlock() { |  | ||||||
| 		console.log(`${type}ing`, domain); |  | ||||||
| 		Promise.try(() => { |  | ||||||
| 			return oauth.apiRequest("/api/v1/admin/domain_blocks", "POST", { |  | ||||||
| 				domain: domain, |  | ||||||
| 				obfuscate: obfuscated, |  | ||||||
| 				private_comment: privateDescription, |  | ||||||
| 				public_comment: publicDescription |  | ||||||
| 			}, "json"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			setDomain(""); |  | ||||||
| 			setPrivateDescription(""); |  | ||||||
| 			setPublicDescription(""); |  | ||||||
| 			setBlocks([json, ...blocks]); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function onDomainChange(e) { |  | ||||||
| 		setDomain(e.target.value); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function onTypeChange(e) { |  | ||||||
| 		setType(e.target.value); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function onKeyDown(e) { |  | ||||||
| 		if (e.key == "Enter") { |  | ||||||
| 			addBlock(); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<React.Fragment> |  | ||||||
| 			<h3>Add Block:</h3> |  | ||||||
| 			<div className="addblock"> |  | ||||||
| 				<input id="domain" placeholder="instance" onChange={onDomainChange} value={domain} onKeyDown={onKeyDown} /> |  | ||||||
| 				<select value={type} onChange={onTypeChange}> |  | ||||||
| 					<option id="suspend">Suspend</option> |  | ||||||
| 					<option id="silence">Silence</option> |  | ||||||
| 				</select> |  | ||||||
| 				<button onClick={addBlock}>Add</button> |  | ||||||
| 				<div> |  | ||||||
| 					<label htmlFor="private">Private description:</label><br/> |  | ||||||
| 					<textarea id="private" value={privateDescription} onChange={(e) => setPrivateDescription(e.target.value)}></textarea> |  | ||||||
| 				</div> |  | ||||||
| 				<div> |  | ||||||
| 					<label htmlFor="public">Public description:</label><br/> |  | ||||||
| 					<textarea id="public" value={publicDescription} onChange={(e) => setPublicDescription(e.target.value)}></textarea> |  | ||||||
| 				</div> |  | ||||||
| 				<div className="single"> |  | ||||||
| 					<label htmlFor="obfuscate">Obfuscate:</label> |  | ||||||
| 					<input id="obfuscate" type="checkbox" value={obfuscated} onChange={(e) => setObfuscated(e.target.checked)}/> |  | ||||||
| 				</div> |  | ||||||
| 			</div> |  | ||||||
| 		</React.Fragment> |  | ||||||
| 	); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // function Blocklist() {
 |  | ||||||
| // 	return (
 |  | ||||||
| // 		<section className="blocklists">
 |  | ||||||
| // 			<h1>Blocklists</h1>
 |  | ||||||
| // 		</section>
 |  | ||||||
| // 	);
 |  | ||||||
| // }
 |  | ||||||
|  | @ -1,64 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const Promise = require("bluebird"); |  | ||||||
| const React = require("react"); |  | ||||||
| const ReactDom = require("react-dom"); |  | ||||||
| 
 |  | ||||||
| const createPanel = require("../lib/panel"); |  | ||||||
| 
 |  | ||||||
| const Settings = require("./settings"); |  | ||||||
| const Blocks = require("./blocks"); |  | ||||||
| 
 |  | ||||||
| require("../base.css"); |  | ||||||
| require("./style.css"); |  | ||||||
| 
 |  | ||||||
| function AdminPanel({oauth}) { |  | ||||||
| 	/*  |  | ||||||
| 		Features: (issue #78) |  | ||||||
| 		- [ ] Instance information updating |  | ||||||
| 			  GET /api/v1/instance PATCH /api/v1/instance |  | ||||||
| 		- [ ] Domain block creation, viewing, and deletion |  | ||||||
| 			  GET /api/v1/admin/domain_blocks |  | ||||||
| 			  POST /api/v1/admin/domain_blocks |  | ||||||
| 			  GET /api/v1/admin/domain_blocks/DOMAIN_BLOCK_ID, DELETE /api/v1/admin/domain_blocks/DOMAIN_BLOCK_ID |  | ||||||
| 		- [ ] Blocklist import/export |  | ||||||
| 			  GET /api/v1/admin/domain_blocks?export=true |  | ||||||
| 			  POST json file as form field domains to /api/v1/admin/domain_blocks |  | ||||||
| 	*/ |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<React.Fragment> |  | ||||||
| 			<Logout oauth={oauth}/> |  | ||||||
| 			<Settings oauth={oauth} /> |  | ||||||
| 			<Blocks oauth={oauth}/> |  | ||||||
| 		</React.Fragment> |  | ||||||
| 	); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function Logout({oauth}) { |  | ||||||
| 	return ( |  | ||||||
| 		<div> |  | ||||||
| 			<button onClick={oauth.logout}>Logout</button> |  | ||||||
| 		</div> |  | ||||||
| 	); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| createPanel("GoToSocial Admin Panel", ["admin"], AdminPanel); |  | ||||||
|  | @ -1,182 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const Promise = require("bluebird"); |  | ||||||
| const React = require("react"); |  | ||||||
| 
 |  | ||||||
| module.exports = function Settings({oauth}) { |  | ||||||
| 	const [info, setInfo] = React.useState({}); |  | ||||||
| 	const [errorMsg, setError] = React.useState(""); |  | ||||||
| 	const [statusMsg, setStatus] = React.useState("Fetching instance info"); |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { |  | ||||||
| 		Promise.try(() => { |  | ||||||
| 			return oauth.apiRequest("/api/v1/instance", "GET"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			setInfo(json); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			setError(e.message); |  | ||||||
| 			setStatus(""); |  | ||||||
| 		}); |  | ||||||
| 	}, []); |  | ||||||
| 
 |  | ||||||
| 	function submit() { |  | ||||||
| 		setStatus("PATCHing"); |  | ||||||
| 		setError(""); |  | ||||||
| 		return Promise.try(() => { |  | ||||||
| 			let formDataInfo = new FormData(); |  | ||||||
| 			Object.entries(info).forEach(([key, val]) => { |  | ||||||
| 				if (key == "contact_account") { |  | ||||||
| 					key = "contact_username"; |  | ||||||
| 					val = val.username; |  | ||||||
| 				} |  | ||||||
| 				if (key == "email") { |  | ||||||
| 					key = "contact_email"; |  | ||||||
| 				} |  | ||||||
| 				if (typeof val != "object") { |  | ||||||
| 					formDataInfo.append(key, val); |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
| 			return oauth.apiRequest("/api/v1/instance", "PATCH", formDataInfo, "form"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			setStatus("Config saved"); |  | ||||||
| 			console.log(json); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			setError(e.message); |  | ||||||
| 			setStatus(""); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<section className="info login"> |  | ||||||
| 			<h1>Instance Information <button onClick={submit}>Save</button></h1> |  | ||||||
| 			<div className="error accent"> |  | ||||||
| 				{errorMsg} |  | ||||||
| 			</div> |  | ||||||
| 			<div> |  | ||||||
| 				{statusMsg} |  | ||||||
| 			</div> |  | ||||||
| 			<form onSubmit={(e) => e.preventDefault()}> |  | ||||||
| 				{editableObject(info)} |  | ||||||
| 			</form> |  | ||||||
| 		</section> |  | ||||||
| 	); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function editableObject(obj, path=[]) { |  | ||||||
| 	const readOnlyKeys = ["uri", "version", "urls_streaming_api", "stats"]; |  | ||||||
| 	const hiddenKeys = ["contact_account_", "urls"]; |  | ||||||
| 	const explicitShownKeys = ["contact_account_username"]; |  | ||||||
| 	const implementedKeys = "title, contact_account_username, email, short_description, description, terms, avatar, header".split(", "); |  | ||||||
| 	const textareaKeys = ["short_description", "description"] |  | ||||||
| 
 |  | ||||||
| 	let listing = Object.entries(obj).map(([key, val]) => { |  | ||||||
| 		let fullkey = [...path, key].join("_"); |  | ||||||
| 
 |  | ||||||
| 		if ( |  | ||||||
| 			hiddenKeys.includes(fullkey) || |  | ||||||
| 			hiddenKeys.includes(path.join("_")+"_") // also match just parent path
 |  | ||||||
| 		) { |  | ||||||
| 			if (!explicitShownKeys.includes(fullkey)) { |  | ||||||
| 				return null; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if (Array.isArray(val)) { |  | ||||||
| 			// FIXME: handle this
 |  | ||||||
| 		} else if (typeof val == "object") { |  | ||||||
| 			return (<React.Fragment key={fullkey}> |  | ||||||
| 				{editableObject(val, [...path, key])} |  | ||||||
| 			</React.Fragment>); |  | ||||||
| 		}  |  | ||||||
| 
 |  | ||||||
| 		let isImplemented = ""; |  | ||||||
| 		if (!implementedKeys.includes(fullkey)) { |  | ||||||
| 			isImplemented = " notImplemented"; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		let isReadOnly = ( |  | ||||||
| 			readOnlyKeys.includes(fullkey) || |  | ||||||
| 			readOnlyKeys.includes(path.join("_")) || |  | ||||||
| 			isImplemented != "" |  | ||||||
| 		); |  | ||||||
| 
 |  | ||||||
| 		let label = key.replace(/_/g, " "); |  | ||||||
| 		if (path.length > 0) { |  | ||||||
| 			label = `\u00A0`.repeat(4 * path.length) + label; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		let inputProps; |  | ||||||
| 		let changeFunc; |  | ||||||
| 		if (val === true || val === false) { |  | ||||||
| 			inputProps = { |  | ||||||
| 				type: "checkbox", |  | ||||||
| 				defaultChecked: val, |  | ||||||
| 				disabled: isReadOnly |  | ||||||
| 			}; |  | ||||||
| 			changeFunc = (e) => e.target.checked; |  | ||||||
| 		} else if (val.length != 0 && !isNaN(val)) { |  | ||||||
| 			inputProps = { |  | ||||||
| 				type: "number", |  | ||||||
| 				defaultValue: val, |  | ||||||
| 				readOnly: isReadOnly |  | ||||||
| 			}; |  | ||||||
| 			changeFunc = (e) => e.target.value; |  | ||||||
| 		} else { |  | ||||||
| 			inputProps = { |  | ||||||
| 				type: "text", |  | ||||||
| 				defaultValue: val, |  | ||||||
| 				readOnly: isReadOnly |  | ||||||
| 			}; |  | ||||||
| 			changeFunc = (e) => e.target.value; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		function setRef(element) { |  | ||||||
| 			if (element != null) { |  | ||||||
| 				element.addEventListener("change", (e) => { |  | ||||||
| 					obj[key] = changeFunc(e); |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		let field; |  | ||||||
| 		if (textareaKeys.includes(fullkey)) { |  | ||||||
| 			field = <textarea className={isImplemented} ref={setRef} {...inputProps}></textarea> |  | ||||||
| 		} else { |  | ||||||
| 			field = <input className={isImplemented} ref={setRef} {...inputProps} /> |  | ||||||
| 		} |  | ||||||
| 		return ( |  | ||||||
| 			<React.Fragment key={fullkey}> |  | ||||||
| 				<label htmlFor={key} className="capitalize">{label}</label> |  | ||||||
| 				<div className={isImplemented}> |  | ||||||
| 					{field} |  | ||||||
| 				</div> |  | ||||||
| 			</React.Fragment> |  | ||||||
| 		); |  | ||||||
| 	}); |  | ||||||
| 	return ( |  | ||||||
| 		<React.Fragment> |  | ||||||
| 			{path != "" && |  | ||||||
| 				<><b>{path}:</b> <span id="filler"></span></> |  | ||||||
| 			} |  | ||||||
| 			{listing} |  | ||||||
| 		</React.Fragment> |  | ||||||
| 	); |  | ||||||
| } |  | ||||||
|  | @ -1,106 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| section.info { |  | ||||||
| 	form { |  | ||||||
| 		grid-template-columns: auto 1fr; |  | ||||||
| 		width: calc(100% - 0.35rem); |  | ||||||
| 
 |  | ||||||
| 		input { |  | ||||||
| 			width: 100%; |  | ||||||
| 			line-height: 1.5rem; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		label, input { |  | ||||||
| 			padding: 0.2rem 0.5rem; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		input[type=checkbox] { |  | ||||||
| 			justify-self: start; |  | ||||||
| 			width: initial; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		input:read-only { |  | ||||||
| 			border: none; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		input:invalid { |  | ||||||
| 			border-color: red; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	textarea { |  | ||||||
| 		width: 100%; |  | ||||||
| 		height: 8rem; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	h1 { |  | ||||||
| 		display: flex; |  | ||||||
| 		justify-content: space-between; |  | ||||||
| 		margin-bottom: 0.5rem; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| section.blocks { |  | ||||||
| 	.overflow { |  | ||||||
| 		max-height: 80vh; |  | ||||||
| 		overflow-y: auto; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	.blocklist { |  | ||||||
| 		display: grid; |  | ||||||
| 		grid-template-columns: auto 1fr auto; |  | ||||||
| 		grid-gap: 0.35rem 0; |  | ||||||
| 		 |  | ||||||
| 		div { |  | ||||||
| 			background: rgb(70, 79, 88); |  | ||||||
| 			padding: 0.2rem 0.4rem; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	.addblock { |  | ||||||
| 		display: grid; |  | ||||||
| 		grid-template-columns: 1fr auto auto; |  | ||||||
| 		grid-gap: 0.35rem; |  | ||||||
| 
 |  | ||||||
| 		input, select { |  | ||||||
| 			font-size: 1.2rem; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		input, select, textarea { |  | ||||||
| 			padding: 0.5rem; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		div { |  | ||||||
| 			grid-column: 1/4; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		div.single input { |  | ||||||
| 			width: initial; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	h3 { |  | ||||||
| 		margin-bottom: 0; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	.controls { |  | ||||||
| 		display: flex; |  | ||||||
| 		gap: 0.5rem; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,67 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| body { |  | ||||||
| 	grid-template-rows: auto 1fr; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .capitalize { |  | ||||||
| 	text-transform: capitalize; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| section { |  | ||||||
| 	margin-bottom: 1rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| input, select, textarea { |  | ||||||
| 	box-sizing: border-box; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .error { |  | ||||||
| 	font-weight: bold; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .hidden { |  | ||||||
| 	display: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .messagebutton { |  | ||||||
| 	margin-top: 1rem; |  | ||||||
| 	display: flex; |  | ||||||
| 	gap: 1rem; |  | ||||||
| 	align-items: center; |  | ||||||
| 
 |  | ||||||
| 	button { |  | ||||||
| 		white-space: nowrap; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .notImplemented { |  | ||||||
| 	border: 2px solid rgb(70, 79, 88); |  | ||||||
| 	background: repeating-linear-gradient( |  | ||||||
| 		-45deg, |  | ||||||
| 		#525c66, |  | ||||||
| 		#525c66 10px, |  | ||||||
| 		rgb(70, 79, 88) 10px, |  | ||||||
| 		rgb(70, 79, 88) 20px |  | ||||||
| 	) !important; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .mono { |  | ||||||
| 	font-family: monospace; |  | ||||||
| } |  | ||||||
|  | @ -1,227 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const Promise = require("bluebird"); |  | ||||||
| 
 |  | ||||||
| function getCurrentUrl() { |  | ||||||
| 	return window.location.origin + window.location.pathname; // strips ?query=string and #hash
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = function oauthClient(config, initState) { |  | ||||||
| 	/* config:  |  | ||||||
| 		instance: instance domain (https://testingtesting123.xyz)
 |  | ||||||
| 		client_name: "GoToSocial Admin Panel" |  | ||||||
| 		scope: [] |  | ||||||
| 		website:  |  | ||||||
| 	*/ |  | ||||||
| 
 |  | ||||||
| 	let state = initState; |  | ||||||
| 	if (initState == undefined) { |  | ||||||
| 		state = localStorage.getItem("oauth"); |  | ||||||
| 		if (state == undefined) { |  | ||||||
| 			state = { |  | ||||||
| 				config |  | ||||||
| 			}; |  | ||||||
| 			storeState(); |  | ||||||
| 		} else { |  | ||||||
| 			state = JSON.parse(state); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function storeState() { |  | ||||||
| 		localStorage.setItem("oauth", JSON.stringify(state)); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	/* register app |  | ||||||
| 		/api/v1/apps |  | ||||||
| 	*/ |  | ||||||
| 	function register() { |  | ||||||
| 		if (state.client_id != undefined) { |  | ||||||
| 			return true; // we already have a registration
 |  | ||||||
| 		} |  | ||||||
| 		let url = new URL(config.instance); |  | ||||||
| 		url.pathname = "/api/v1/apps"; |  | ||||||
| 
 |  | ||||||
| 		return fetch(url.href, { |  | ||||||
| 			method: "POST", |  | ||||||
| 			headers: { |  | ||||||
| 				'Content-Type': 'application/json' |  | ||||||
| 			}, |  | ||||||
| 			body: JSON.stringify({ |  | ||||||
| 				client_name: config.client_name, |  | ||||||
| 				redirect_uris: getCurrentUrl(), |  | ||||||
| 				scopes: config.scope.join(" "), |  | ||||||
| 				website: getCurrentUrl() |  | ||||||
| 			}) |  | ||||||
| 		}).then((res) => { |  | ||||||
| 			if (res.status != 200) { |  | ||||||
| 				throw res; |  | ||||||
| 			} |  | ||||||
| 			return res.json(); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			state.client_id = json.client_id; |  | ||||||
| 			state.client_secret = json.client_secret; |  | ||||||
| 			storeState(); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	/* authorize: |  | ||||||
| 		/oauth/authorize |  | ||||||
| 			?client_id=CLIENT_ID |  | ||||||
| 			&redirect_uri=window.location.href |  | ||||||
| 			&response_type=code |  | ||||||
| 			&scope=admin |  | ||||||
| 	*/ |  | ||||||
| 	function authorize() { |  | ||||||
| 		let url = new URL(config.instance); |  | ||||||
| 		url.pathname = "/oauth/authorize"; |  | ||||||
| 		url.searchParams.set("client_id", state.client_id); |  | ||||||
| 		url.searchParams.set("redirect_uri", getCurrentUrl()); |  | ||||||
| 		url.searchParams.set("response_type", "code"); |  | ||||||
| 		url.searchParams.set("scope", config.scope.join(" ")); |  | ||||||
| 
 |  | ||||||
| 		window.location.assign(url.href); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	function callback() { |  | ||||||
| 		if (state.access_token != undefined) { |  | ||||||
| 			return; // we're already done :)
 |  | ||||||
| 		} |  | ||||||
| 		let params = (new URL(window.location)).searchParams; |  | ||||||
| 	 |  | ||||||
| 		let token = params.get("code"); |  | ||||||
| 		if (token != null) { |  | ||||||
| 			console.log("got token callback:", token); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return authorizeToken(token) |  | ||||||
| 			.catch((e) => { |  | ||||||
| 				console.log("Error processing oauth callback:", e); |  | ||||||
| 				logout(); // just to be sure
 |  | ||||||
| 			}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function authorizeToken(token) { |  | ||||||
| 		let url = new URL(config.instance); |  | ||||||
| 		url.pathname = "/oauth/token"; |  | ||||||
| 		return fetch(url.href, { |  | ||||||
| 			method: "POST", |  | ||||||
| 			headers: { |  | ||||||
| 				"Content-Type": "application/json" |  | ||||||
| 			}, |  | ||||||
| 			body: JSON.stringify({ |  | ||||||
| 				client_id: state.client_id, |  | ||||||
| 				client_secret: state.client_secret, |  | ||||||
| 				redirect_uri: getCurrentUrl(), |  | ||||||
| 				grant_type: "authorization_code", |  | ||||||
| 				code: token |  | ||||||
| 			}) |  | ||||||
| 		}).then((res) => { |  | ||||||
| 			if (res.status != 200) { |  | ||||||
| 				throw res; |  | ||||||
| 			} |  | ||||||
| 			return res.json(); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			state.access_token = json.access_token; |  | ||||||
| 			storeState(); |  | ||||||
| 			window.location = getCurrentUrl(); // clear ?token=
 |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function isAuthorized() { |  | ||||||
| 		return (state.access_token != undefined); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function apiRequest(path, method, data, type="json", accept="json") { |  | ||||||
| 		if (!isAuthorized()) { |  | ||||||
| 			throw new Error("Not Authenticated"); |  | ||||||
| 		} |  | ||||||
| 		let url = new URL(config.instance); |  | ||||||
| 		let [p, s] = path.split("?"); |  | ||||||
| 		url.pathname = p; |  | ||||||
| 		if (s != undefined) { |  | ||||||
| 			url.search = s; |  | ||||||
| 		} |  | ||||||
| 		let headers = { |  | ||||||
| 			"Authorization": `Bearer ${state.access_token}`, |  | ||||||
| 			"Accept": accept == "json" ? "application/json" : "*/*" |  | ||||||
| 		}; |  | ||||||
| 		let body = data; |  | ||||||
| 		if (type == "json" && body != undefined) { |  | ||||||
| 			headers["Content-Type"] = "application/json"; |  | ||||||
| 			body = JSON.stringify(data); |  | ||||||
| 		} |  | ||||||
| 		return fetch(url.href, { |  | ||||||
| 			method, |  | ||||||
| 			headers, |  | ||||||
| 			body |  | ||||||
| 		}).then((res) => { |  | ||||||
| 			return Promise.all([res.json(), res]); |  | ||||||
| 		}).then(([json, res]) => { |  | ||||||
| 			if (res.status != 200) { |  | ||||||
| 				if (json.error) { |  | ||||||
| 					throw new Error(json.error); |  | ||||||
| 				} else { |  | ||||||
| 					throw new Error(`${res.status}: ${res.statusText}`); |  | ||||||
| 				} |  | ||||||
| 			} else { |  | ||||||
| 				return json; |  | ||||||
| 			} |  | ||||||
| 		}).catch(e => { |  | ||||||
| 			if (e instanceof SyntaxError) { |  | ||||||
| 				throw new Error("Error: The GtS API returned a non-json error. This usually means a network problem, or an issue with your instance's reverse proxy configuration.", {cause: e}); |  | ||||||
| 			} else { |  | ||||||
| 				throw e; |  | ||||||
| 			} |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function logout() { |  | ||||||
| 		let url = new URL(config.instance); |  | ||||||
| 		url.pathname = "/oauth/revoke"; |  | ||||||
| 		return fetch(url.href, { |  | ||||||
| 			method: "POST", |  | ||||||
| 			headers: { |  | ||||||
| 				"Content-Type": "application/json" |  | ||||||
| 			}, |  | ||||||
| 			body: JSON.stringify({ |  | ||||||
| 				client_id: state.client_id, |  | ||||||
| 				client_secret: state.client_secret, |  | ||||||
| 				token: state.access_token, |  | ||||||
| 			}) |  | ||||||
| 		}).then((res) => { |  | ||||||
| 			if (res.status != 200) { |  | ||||||
| 				// GoToSocial doesn't actually implement this route yet,
 |  | ||||||
| 				// so error is to be expected
 |  | ||||||
| 				return; |  | ||||||
| 			} |  | ||||||
| 			return res.json(); |  | ||||||
| 		}).catch(() => { |  | ||||||
| 			// see above
 |  | ||||||
| 		}).then(() => { |  | ||||||
| 			localStorage.removeItem("oauth"); |  | ||||||
| 			window.location = getCurrentUrl(); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return { |  | ||||||
| 		register, authorize, callback, isAuthorized, apiRequest, logout |  | ||||||
| 	}; |  | ||||||
| }; |  | ||||||
|  | @ -1,151 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const React = require("react"); |  | ||||||
| const Promise = require("bluebird"); |  | ||||||
| 
 |  | ||||||
| const Submit = require("../../lib/submit"); |  | ||||||
| 
 |  | ||||||
| module.exports = function Basic({oauth, account, allowCustomCSS}) { |  | ||||||
| 	const [errorMsg, setError] = React.useState(""); |  | ||||||
| 	const [statusMsg, setStatus] = React.useState(""); |  | ||||||
| 
 |  | ||||||
| 	const [headerFile, setHeaderFile] = React.useState(undefined); |  | ||||||
| 	const [headerSrc, setHeaderSrc] = React.useState(""); |  | ||||||
| 
 |  | ||||||
| 	const [avatarFile, setAvatarFile] = React.useState(undefined); |  | ||||||
| 	const [avatarSrc, setAvatarSrc] = React.useState(""); |  | ||||||
| 
 |  | ||||||
| 	const [displayName, setDisplayName] = React.useState(""); |  | ||||||
| 	const [bio, setBio] = React.useState(""); |  | ||||||
| 	const [locked, setLocked] = React.useState(false); |  | ||||||
| 	const [customCSS, setCustomCSS] = React.useState(""); |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { |  | ||||||
| 		setHeaderSrc(account.header); |  | ||||||
| 		setAvatarSrc(account.avatar); |  | ||||||
| 
 |  | ||||||
| 		setDisplayName(account.display_name); |  | ||||||
| 		setBio(account.source ? account.source.note : ""); |  | ||||||
| 		setLocked(account.locked); |  | ||||||
| 		setCustomCSS((allowCustomCSS && account.custom_css) ? account.custom_css : ""); |  | ||||||
| 	}, [account, setHeaderSrc, setAvatarSrc, setDisplayName, setBio, setLocked, setCustomCSS]); |  | ||||||
| 
 |  | ||||||
| 	const headerOnChange = (e) => { |  | ||||||
| 		setHeaderFile(e.target.files[0]); |  | ||||||
| 		setHeaderSrc(URL.createObjectURL(e.target.files[0])); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const avatarOnChange = (e) => { |  | ||||||
| 		setAvatarFile(e.target.files[0]); |  | ||||||
| 		setAvatarSrc(URL.createObjectURL(e.target.files[0])); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const submit = (e) => { |  | ||||||
| 		e.preventDefault(); |  | ||||||
| 
 |  | ||||||
| 		setStatus("PATCHing"); |  | ||||||
| 		setError(""); |  | ||||||
| 		return Promise.try(() => { |  | ||||||
| 			let formDataInfo = new FormData(); |  | ||||||
| 
 |  | ||||||
| 			if (headerFile) { |  | ||||||
| 				formDataInfo.set("header", headerFile); |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if (avatarFile) { |  | ||||||
| 				formDataInfo.set("avatar", avatarFile); |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			formDataInfo.set("display_name", displayName); |  | ||||||
| 			formDataInfo.set("note", bio); |  | ||||||
| 			formDataInfo.set("locked", locked); |  | ||||||
| 			 |  | ||||||
| 			if (allowCustomCSS) { |  | ||||||
| 				formDataInfo.set("custom_css", customCSS); |  | ||||||
| 			} |  | ||||||
| 			 |  | ||||||
| 			return oauth.apiRequest("/api/v1/accounts/update_credentials", "PATCH", formDataInfo, "form"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			setStatus("Saved!"); |  | ||||||
| 
 |  | ||||||
| 			setHeaderSrc(json.header); |  | ||||||
| 			setAvatarSrc(json.avatar); |  | ||||||
| 
 |  | ||||||
| 			setDisplayName(json.display_name); |  | ||||||
| 			setBio(json.source.note); |  | ||||||
| 			setLocked(json.locked); |  | ||||||
| 			setCustomCSS(allowCustomCSS && json.custom_css ? json.custom_css : ""); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			setError(e.message); |  | ||||||
| 			setStatus(""); |  | ||||||
| 		}); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<section className="basic"> |  | ||||||
| 			<h1>@{account.username}'s Profile Info</h1> |  | ||||||
| 			<form> |  | ||||||
| 				<div className="labelinput"> |  | ||||||
| 					<label htmlFor="header">Header</label> |  | ||||||
| 					<div className="border"> |  | ||||||
| 						<img className="headerpreview" src={headerSrc} alt={headerSrc ? `header image for ${account.username}` : "None set"}/> |  | ||||||
| 						<div> |  | ||||||
| 							<label htmlFor="header" className="file-input button">Browse…</label> |  | ||||||
| 							<span>{headerFile ? headerFile.name : ""}</span> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 					<input className="hidden" id="header" type="file" accept="image/*" onChange={headerOnChange}/> |  | ||||||
| 				</div> |  | ||||||
| 				<div className="labelinput"> |  | ||||||
| 					<label htmlFor="avatar">Avatar</label> |  | ||||||
| 					<div className="border"> |  | ||||||
| 						<img className="avatarpreview" src={avatarSrc} alt={headerSrc ? `avatar image for ${account.username}` : "None set"}/> |  | ||||||
| 						<div> |  | ||||||
| 							<label htmlFor="avatar" className="file-input button">Browse…</label> |  | ||||||
| 							<span>{avatarFile ? avatarFile.name : ""}</span> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 					<input className="hidden" id="avatar" type="file" accept="image/*" onChange={avatarOnChange}/> |  | ||||||
| 				</div> |  | ||||||
| 				<div className="labelinput"> |  | ||||||
| 					<label htmlFor="displayname">Display Name</label> |  | ||||||
| 					<input id="displayname" type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="A GoToSocial user"/> |  | ||||||
| 				</div> |  | ||||||
| 				<div className="labelinput"> |  | ||||||
| 					<label htmlFor="bio">Bio</label> |  | ||||||
| 					<textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."/> |  | ||||||
| 				</div> |  | ||||||
| 				{ !allowCustomCSS ? null :   |  | ||||||
| 					<div className="labelinput"> |  | ||||||
| 						<label htmlFor="customcss">Custom CSS</label> |  | ||||||
| 						<textarea className="mono" id="customcss" value={customCSS} onChange={(e) => setCustomCSS(e.target.value)}/> |  | ||||||
| 						<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom CSS (opens in a new tab)</a> |  | ||||||
| 					</div> |  | ||||||
| 				} |  | ||||||
| 				<div className="labelcheckbox"> |  | ||||||
| 					<label htmlFor="locked">Manually approve follow requests</label> |  | ||||||
| 					<input id="locked" type="checkbox" checked={locked} onChange={(e) => setLocked(e.target.checked)}/> |  | ||||||
| 				</div> |  | ||||||
| 				<Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg}/> |  | ||||||
| 			</form> |  | ||||||
| 		</section> |  | ||||||
| 	); |  | ||||||
| }; |  | ||||||
|  | @ -1,76 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const Promise = require("bluebird"); |  | ||||||
| const React = require("react"); |  | ||||||
| const ReactDom = require("react-dom"); |  | ||||||
| 
 |  | ||||||
| const createPanel = require("../lib/panel"); |  | ||||||
| 
 |  | ||||||
| const Basic = require("./basic"); |  | ||||||
| const Posts = require("./posts"); |  | ||||||
| const Security = require("./security"); |  | ||||||
| 
 |  | ||||||
| require("../base.css"); |  | ||||||
| require("./style.css"); |  | ||||||
| 
 |  | ||||||
| function UserPanel({oauth}) { |  | ||||||
| 	const [account, setAccount] = React.useState({}); |  | ||||||
| 	const [allowCustomCSS, setAllowCustomCSS] = React.useState(false); |  | ||||||
| 	const [errorMsg, setError] = React.useState(""); |  | ||||||
| 	const [statusMsg, setStatus] = React.useState("Fetching user info"); |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { |  | ||||||
| 
 |  | ||||||
| 	}, [oauth, setAllowCustomCSS, setError, setStatus]); |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { |  | ||||||
| 		Promise.try(() => { |  | ||||||
| 			return oauth.apiRequest("/api/v1/instance", "GET"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			setAllowCustomCSS(json.configuration.accounts.allow_custom_css); |  | ||||||
| 			Promise.try(() => { |  | ||||||
| 				return oauth.apiRequest("/api/v1/accounts/verify_credentials", "GET"); |  | ||||||
| 			}).then((json) => { |  | ||||||
| 				setAccount(json); |  | ||||||
| 			}).catch((e) => { |  | ||||||
| 				setError(e.message); |  | ||||||
| 				setStatus(""); |  | ||||||
| 			}); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			setError(e.message); |  | ||||||
| 			setStatus(""); |  | ||||||
| 		}); |  | ||||||
| 
 |  | ||||||
| 	}, [oauth, setAllowCustomCSS, setAccount, setError, setStatus]); |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<React.Fragment> |  | ||||||
| 			<div> |  | ||||||
| 				<button className="logout" onClick={oauth.logout}>Log out of settings panel</button> |  | ||||||
| 			</div> |  | ||||||
| 			<Basic oauth={oauth} account={account} allowCustomCSS={allowCustomCSS}/> |  | ||||||
| 			<Posts oauth={oauth} account={account}/> |  | ||||||
| 			<Security oauth={oauth}/> |  | ||||||
| 		</React.Fragment> |  | ||||||
| 	); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| createPanel("GoToSocial User Panel", ["read write"], UserPanel); |  | ||||||
|  | @ -1,107 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const React = require("react"); |  | ||||||
| const Promise = require("bluebird"); |  | ||||||
| 
 |  | ||||||
| const Languages = require("./languages"); |  | ||||||
| const Submit = require("../../lib/submit"); |  | ||||||
| 
 |  | ||||||
| module.exports = function Posts({oauth, account}) { |  | ||||||
| 	const [errorMsg, setError] = React.useState(""); |  | ||||||
| 	const [statusMsg, setStatus] = React.useState(""); |  | ||||||
| 
 |  | ||||||
| 	const [language, setLanguage] = React.useState(""); |  | ||||||
| 	const [privacy, setPrivacy] = React.useState(""); |  | ||||||
| 	const [format, setFormat] = React.useState(""); |  | ||||||
| 	const [sensitive, setSensitive] = React.useState(false); |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { |  | ||||||
| 		if (account.source) { |  | ||||||
| 			setLanguage(account.source.language.toUpperCase()); |  | ||||||
| 			setPrivacy(account.source.privacy); |  | ||||||
| 			setSensitive(account.source.sensitive ? account.source.sensitive : false); |  | ||||||
| 			setFormat(account.source.status_format ? account.source.status_format : "plain"); |  | ||||||
| 		} |  | ||||||
|          |  | ||||||
| 	}, [account, setSensitive, setPrivacy]); |  | ||||||
| 
 |  | ||||||
| 	const submit = (e) => { |  | ||||||
| 		e.preventDefault(); |  | ||||||
| 
 |  | ||||||
| 		setStatus("PATCHing"); |  | ||||||
| 		setError(""); |  | ||||||
| 		return Promise.try(() => { |  | ||||||
| 			let formDataInfo = new FormData(); |  | ||||||
| 
 |  | ||||||
| 			formDataInfo.set("source[language]", language); |  | ||||||
| 			formDataInfo.set("source[privacy]", privacy); |  | ||||||
| 			formDataInfo.set("source[sensitive]", sensitive); |  | ||||||
| 			formDataInfo.set("source[status_format]", format); |  | ||||||
| 
 |  | ||||||
| 			return oauth.apiRequest("/api/v1/accounts/update_credentials", "PATCH", formDataInfo, "form"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			setStatus("Saved!"); |  | ||||||
| 			setLanguage(json.source.language.toUpperCase()); |  | ||||||
| 			setPrivacy(json.source.privacy); |  | ||||||
| 			setSensitive(json.source.sensitive ? json.source.sensitive : false); |  | ||||||
| 			setFormat(json.source.status_format ? json.source.status_format : "plain"); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			setError(e.message); |  | ||||||
| 			setStatus(""); |  | ||||||
| 		}); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<section className="posts"> |  | ||||||
| 			<h1>Post Settings</h1> |  | ||||||
| 			<form> |  | ||||||
| 				<div className="labelselect"> |  | ||||||
| 					<label htmlFor="language">Default post language</label> |  | ||||||
| 					<select id="language" autoComplete="language" value={language} onChange={(e) => setLanguage(e.target.value)}> |  | ||||||
| 						<Languages /> |  | ||||||
| 					</select> |  | ||||||
| 				</div> |  | ||||||
| 				<div className="labelselect"> |  | ||||||
| 					<label htmlFor="privacy">Default post privacy</label> |  | ||||||
| 					<select id="privacy" value={privacy} onChange={(e) => setPrivacy(e.target.value)}> |  | ||||||
| 						<option value="private">Private / followers-only)</option> |  | ||||||
| 						<option value="unlisted">Unlisted</option> |  | ||||||
| 						<option value="public">Public</option> |  | ||||||
| 					</select> |  | ||||||
| 					<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a> |  | ||||||
| 				</div> |  | ||||||
| 				<div className="labelselect"> |  | ||||||
| 					<label htmlFor="format">Default post format</label> |  | ||||||
| 					<select id="format" value={format} onChange={(e) => setFormat(e.target.value)}> |  | ||||||
| 						<option value="plain">Plain (default)</option> |  | ||||||
| 						<option value="markdown">Markdown</option> |  | ||||||
| 					</select> |  | ||||||
| 					<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a> |  | ||||||
| 				</div>				 |  | ||||||
| 				<div className="labelcheckbox"> |  | ||||||
| 					<label htmlFor="sensitive">Mark my posts as sensitive by default</label> |  | ||||||
| 					<input id="sensitive" type="checkbox" checked={sensitive} onChange={(e) => setSensitive(e.target.checked)}/> |  | ||||||
| 				</div> |  | ||||||
| 				<Submit onClick={submit} label="Save post settings" errorMsg={errorMsg} statusMsg={statusMsg}/> |  | ||||||
| 			</form> |  | ||||||
| 		</section> |  | ||||||
| 	); |  | ||||||
| }; |  | ||||||
|  | @ -1,80 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| "use strict"; |  | ||||||
| 
 |  | ||||||
| const React = require("react"); |  | ||||||
| const Promise = require("bluebird"); |  | ||||||
| 
 |  | ||||||
| const Submit = require("../../lib/submit"); |  | ||||||
| 
 |  | ||||||
| module.exports = function Security({oauth}) { |  | ||||||
| 	const [errorMsg, setError] = React.useState(""); |  | ||||||
| 	const [statusMsg, setStatus] = React.useState(""); |  | ||||||
| 
 |  | ||||||
| 	const [oldPassword, setOldPassword] = React.useState(""); |  | ||||||
| 	const [newPassword, setNewPassword] = React.useState(""); |  | ||||||
| 	const [newPasswordConfirm, setNewPasswordConfirm] = React.useState(""); |  | ||||||
| 
 |  | ||||||
| 	const submit = (e) => { |  | ||||||
| 		e.preventDefault(); |  | ||||||
| 
 |  | ||||||
| 		if (newPassword !== newPasswordConfirm) { |  | ||||||
| 			setError("New password and confirm new password did not match!"); |  | ||||||
| 			return; |  | ||||||
| 		} |  | ||||||
|        |  | ||||||
| 		setStatus("PATCHing"); |  | ||||||
| 		setError(""); |  | ||||||
| 		return Promise.try(() => { |  | ||||||
| 			let formDataInfo = new FormData(); |  | ||||||
| 			formDataInfo.set("old_password", oldPassword); |  | ||||||
| 			formDataInfo.set("new_password", newPassword); |  | ||||||
| 			return oauth.apiRequest("/api/v1/user/password_change", "POST", formDataInfo, "form"); |  | ||||||
| 		}).then((json) => { |  | ||||||
| 			setStatus("Saved!"); |  | ||||||
| 			setOldPassword(""); |  | ||||||
| 			setNewPassword(""); |  | ||||||
| 			setNewPasswordConfirm(""); |  | ||||||
| 		}).catch((e) => { |  | ||||||
| 			setError(e.message); |  | ||||||
| 			setStatus(""); |  | ||||||
| 		}); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<section className="security"> |  | ||||||
| 			<h1>Password Change</h1> |  | ||||||
| 			<form> |  | ||||||
| 				<div className="labelinput"> |  | ||||||
| 					<label htmlFor="password">Current password</label> |  | ||||||
| 					<input name="password" id="password" type="password" autoComplete="current-password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} /> |  | ||||||
| 				</div> |  | ||||||
| 				<div className="labelinput"> |  | ||||||
| 					<label htmlFor="new-password">New password</label> |  | ||||||
| 					<input name="new-password" id="new-password" type="password" autoComplete="new-password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} /> |  | ||||||
| 				</div> |  | ||||||
| 				<div className="labelinput"> |  | ||||||
| 					<label htmlFor="confirm-new-password">Confirm new password</label> |  | ||||||
| 					<input name="confirm-new-password" id="confirm-new-password" type="password" autoComplete="new-password" value={newPasswordConfirm} onChange={(e) => setNewPasswordConfirm(e.target.value)} /> |  | ||||||
| 				</div> |  | ||||||
| 				<Submit onClick={submit} label="Save new password" errorMsg={errorMsg} statusMsg={statusMsg}/> |  | ||||||
| 			</form> |  | ||||||
| 		</section> |  | ||||||
| 	); |  | ||||||
| }; |  | ||||||
|  | @ -1,118 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| section.basic, section.posts, section.security { |  | ||||||
| 	form { |  | ||||||
| 		display: flex; |  | ||||||
| 		flex-direction: column; |  | ||||||
| 		gap: 1rem; |  | ||||||
| 
 |  | ||||||
| 		input, textarea { |  | ||||||
| 			width: 100%; |  | ||||||
| 			line-height: 1.5rem; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		input[type=checkbox] { |  | ||||||
| 			justify-self: start; |  | ||||||
| 			width: initial; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		input:read-only { |  | ||||||
| 			border: none; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		input:invalid { |  | ||||||
| 			border-color: red; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	textarea { |  | ||||||
| 		width: 100%; |  | ||||||
| 		height: 8rem; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	h1 { |  | ||||||
| 		margin-bottom: 0.5rem; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	img { |  | ||||||
| 		display: flex; |  | ||||||
| 		justify-content: center; |  | ||||||
| 		align-items: center; |  | ||||||
| 		border: $boxshadow_border; |  | ||||||
| 		box-shadow: $box-shadow; |  | ||||||
| 		object-fit: cover; |  | ||||||
| 		border-radius: 0.2rem; |  | ||||||
| 		box-sizing: border-box; |  | ||||||
| 		margin-bottom: 0.5rem; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	.avatarpreview { |  | ||||||
| 		height: 8.5rem; |  | ||||||
| 		width: 8.5rem; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	.headerpreview { |  | ||||||
| 		width: 100%; |  | ||||||
| 		aspect-ratio: 3 / 1; |  | ||||||
| 		overflow: hidden; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	.moreinfolink { |  | ||||||
| 		font-size: 0.9em; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .labelinput .border { |  | ||||||
| 	border-radius: 0.2rem; |  | ||||||
| 	border: 0.15rem solid $border_accent; |  | ||||||
| 	padding: 0.3rem; |  | ||||||
| 	display: flex; |  | ||||||
| 	flex-direction: column; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .file-input.button { |  | ||||||
| 	display: inline-block; |  | ||||||
| 	font-size: 1rem; |  | ||||||
| 	font-weight: normal; |  | ||||||
| 	padding: 0.3rem 0.3rem; |  | ||||||
| 	align-self: flex-start; |  | ||||||
| 	/* background: $border_accent; */ |  | ||||||
| 	margin-right: 0.2rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .labelinput, .labelselect { |  | ||||||
| 	display: flex; |  | ||||||
| 	flex-direction: column; |  | ||||||
| 	gap: 0.4rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .labelcheckbox { |  | ||||||
| 	display: flex; |  | ||||||
| 	gap: 0.4rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .titlesave { |  | ||||||
| 	display: flex; |  | ||||||
| 	flex-wrap: wrap; |  | ||||||
| 	gap: 0.4rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .logout { |  | ||||||
| 	margin-bottom: 2rem; |  | ||||||
| } |  | ||||||
							
								
								
									
										61
									
								
								web/source/settings-panel/admin/actions.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								web/source/settings-panel/admin/actions.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | 
 | ||||||
|  | const Submit = require("../components/submit"); | ||||||
|  | 
 | ||||||
|  | const api = require("../lib/api"); | ||||||
|  | const submit = require("../lib/submit"); | ||||||
|  | 
 | ||||||
|  | module.exports = function AdminActionPanel() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 
 | ||||||
|  | 	const [days, setDays] = React.useState(30); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 	const [statusMsg, setStatus] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	const removeMedia = submit( | ||||||
|  | 		() => dispatch(api.admin.mediaCleanup(days)), | ||||||
|  | 		{setStatus, setError} | ||||||
|  | 	); | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<> | ||||||
|  | 			<h1>Admin Actions</h1> | ||||||
|  | 			<div> | ||||||
|  | 				<h2>Media cleanup</h2> | ||||||
|  | 				<p> | ||||||
|  | 					Clean up remote media older than the specified number of days. | ||||||
|  | 					If the remote instance is still online they will be refetched when needed. | ||||||
|  | 					Also cleans up unused headers and avatars from the media cache. | ||||||
|  | 				</p> | ||||||
|  | 				<div> | ||||||
|  | 					<label htmlFor="days">Days: </label> | ||||||
|  | 					<input id="days" type="number" value={days} onChange={(e) => setDays(e.target.value)}/> | ||||||
|  | 				</div> | ||||||
|  | 				<Submit onClick={removeMedia} label="Remove media" errorMsg={errorMsg} statusMsg={statusMsg} /> | ||||||
|  | 			</div> | ||||||
|  | 		</> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
							
								
								
									
										212
									
								
								web/source/settings-panel/admin/emoji.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								web/source/settings-panel/admin/emoji.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,212 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter"); | ||||||
|  | 
 | ||||||
|  | const Submit = require("../components/submit"); | ||||||
|  | const FakeToot = require("../components/fake-toot"); | ||||||
|  | const { formFields } = require("../components/form-fields"); | ||||||
|  | 
 | ||||||
|  | const api = require("../lib/api"); | ||||||
|  | const adminActions = require("../redux/reducers/admin").actions; | ||||||
|  | const submit = require("../lib/submit"); | ||||||
|  | 
 | ||||||
|  | const base = "/settings/admin/custom-emoji"; | ||||||
|  | 
 | ||||||
|  | module.exports = function CustomEmoji() { | ||||||
|  | 	return ( | ||||||
|  | 		<Switch> | ||||||
|  | 			<Route path={`${base}/:emojiId`}> | ||||||
|  | 				<EmojiDetailWrapped /> | ||||||
|  | 			</Route> | ||||||
|  | 			<EmojiOverview /> | ||||||
|  | 		</Switch> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function EmojiOverview() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 	const [loaded, setLoaded] = React.useState(false); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	React.useEffect(() => { | ||||||
|  | 		if (!loaded) { | ||||||
|  | 			Promise.try(() => { | ||||||
|  | 				return dispatch(api.admin.fetchCustomEmoji()); | ||||||
|  | 			}).then(() => { | ||||||
|  | 				setLoaded(true); | ||||||
|  | 			}).catch((e) => { | ||||||
|  | 				setLoaded(true); | ||||||
|  | 				setError(e.message); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}, []); | ||||||
|  | 
 | ||||||
|  | 	if (!loaded) { | ||||||
|  | 		return ( | ||||||
|  | 			<> | ||||||
|  | 				<h1>Custom Emoji</h1> | ||||||
|  | 				Loading... | ||||||
|  | 			</> | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<> | ||||||
|  | 			<h1>Custom Emoji</h1> | ||||||
|  | 			<EmojiList/> | ||||||
|  | 			<NewEmoji/> | ||||||
|  | 			{errorMsg.length > 0 &&  | ||||||
|  | 				<div className="error accent">{errorMsg}</div> | ||||||
|  | 			} | ||||||
|  | 		</> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const NewEmojiForm = formFields(adminActions.updateNewEmojiVal, (state) => state.admin.newEmoji); | ||||||
|  | function NewEmoji() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 	const newEmojiForm = Redux.useSelector((state) => state.admin.newEmoji); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 	const [statusMsg, setStatus] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	const uploadEmoji = submit( | ||||||
|  | 		() => dispatch(api.admin.newEmoji()), | ||||||
|  | 		{ | ||||||
|  | 			setStatus, setError, | ||||||
|  | 			onSuccess: function() { | ||||||
|  | 				URL.revokeObjectURL(newEmojiForm.image); | ||||||
|  | 				return Promise.all([ | ||||||
|  | 					dispatch(adminActions.updateNewEmojiVal(["image", undefined])), | ||||||
|  | 					dispatch(adminActions.updateNewEmojiVal(["imageFile", undefined])), | ||||||
|  | 					dispatch(adminActions.updateNewEmojiVal(["shortcode", ""])), | ||||||
|  | 				]); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	); | ||||||
|  | 
 | ||||||
|  | 	React.useEffect(() => { | ||||||
|  | 		if (newEmojiForm.shortcode.length == 0) { | ||||||
|  | 			if (newEmojiForm.imageFile != undefined) { | ||||||
|  | 				let [name, ext] = newEmojiForm.imageFile.name.split("."); | ||||||
|  | 				dispatch(adminActions.updateNewEmojiVal(["shortcode", name])); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	let emojiOrShortcode = `:${newEmojiForm.shortcode}:`; | ||||||
|  | 
 | ||||||
|  | 	if (newEmojiForm.image != undefined) { | ||||||
|  | 		emojiOrShortcode = <img | ||||||
|  | 			className="emoji" | ||||||
|  | 			src={newEmojiForm.image} | ||||||
|  | 			title={`:${newEmojiForm.shortcode}:`} | ||||||
|  | 			alt={newEmojiForm.shortcode} | ||||||
|  | 		/>; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<div> | ||||||
|  | 			<h2>Add new custom emoji</h2> | ||||||
|  | 
 | ||||||
|  | 			<FakeToot> | ||||||
|  | 				Look at this new custom emoji {emojiOrShortcode} isn't it cool? | ||||||
|  | 			</FakeToot> | ||||||
|  | 
 | ||||||
|  | 			<NewEmojiForm.File | ||||||
|  | 				id="image" | ||||||
|  | 				name="Image" | ||||||
|  | 				fileType="image/png,image/gif" | ||||||
|  | 				showSize={true} | ||||||
|  | 				maxSize={50 * 1000} | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<NewEmojiForm.TextInput | ||||||
|  | 				id="shortcode" | ||||||
|  | 				name="Shortcode (without : :), must be unique on the instance" | ||||||
|  | 				placeHolder="blobcat" | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<Submit onClick={uploadEmoji} label="Upload" errorMsg={errorMsg} statusMsg={statusMsg} /> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function EmojiList() { | ||||||
|  | 	const emoji = Redux.useSelector((state) => state.admin.emoji); | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<div> | ||||||
|  | 			<h2>Overview</h2> | ||||||
|  | 			<div className="list emoji-list"> | ||||||
|  | 				{Object.entries(emoji).map(([category, entries]) => { | ||||||
|  | 					return <EmojiCategory key={category} category={category} entries={entries}/>; | ||||||
|  | 				})} | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function EmojiCategory({category, entries}) { | ||||||
|  | 	return ( | ||||||
|  | 		<div className="entry"> | ||||||
|  | 			<b>{category}</b> | ||||||
|  | 			<div className="emoji-group"> | ||||||
|  | 				{entries.map((e) => { | ||||||
|  | 					return ( | ||||||
|  | 						// <Link key={e.static_url} to={`${base}/${e.shortcode}`}>
 | ||||||
|  | 						<Link key={e.static_url} to={`${base}`}> | ||||||
|  | 							<a> | ||||||
|  | 								<img src={e.static_url} alt={e.shortcode} title={`:${e.shortcode}:`}/> | ||||||
|  | 							</a> | ||||||
|  | 						</Link> | ||||||
|  | 					); | ||||||
|  | 				})} | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function EmojiDetailWrapped() { | ||||||
|  | 	/* We wrap the component to generate formFields with a setter depending on the domain | ||||||
|  | 		 if formFields() is used inside the same component that is re-rendered with their state, | ||||||
|  | 		 inputs get re-created on every change, causing them to lose focus, and bad performance | ||||||
|  | 	*/ | ||||||
|  | 	let [_match, {emojiId}] = useRoute(`${base}/:emojiId`); | ||||||
|  | 
 | ||||||
|  | 	function alterEmoji([key, val]) { | ||||||
|  | 		return adminActions.updateDomainBlockVal([emojiId, key, val]); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const fields = formFields(alterEmoji, (state) => state.admin.blockedInstances[emojiId]); | ||||||
|  | 
 | ||||||
|  | 	return <EmojiDetail id={emojiId} Form={fields} />; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function EmojiDetail({id, Form}) { | ||||||
|  | 	return ( | ||||||
|  | 		"Not implemented yet" | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										382
									
								
								web/source/settings-panel/admin/federation.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								web/source/settings-panel/admin/federation.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,382 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter"); | ||||||
|  | const fileDownload = require("js-file-download"); | ||||||
|  | 
 | ||||||
|  | const { formFields } = require("../components/form-fields"); | ||||||
|  | 
 | ||||||
|  | const api = require("../lib/api"); | ||||||
|  | const adminActions = require("../redux/reducers/admin").actions; | ||||||
|  | const submit = require("../lib/submit"); | ||||||
|  | 
 | ||||||
|  | const base = "/settings/admin/federation"; | ||||||
|  | 
 | ||||||
|  | // const {
 | ||||||
|  | // 	TextInput,
 | ||||||
|  | // 	TextArea,
 | ||||||
|  | // 	File
 | ||||||
|  | // } = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings);
 | ||||||
|  | 
 | ||||||
|  | module.exports = function AdminSettings() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 	// const instance = Redux.useSelector(state => state.instances.adminSettings);
 | ||||||
|  | 	const loadedBlockedInstances = Redux.useSelector(state => state.admin.loadedBlockedInstances); | ||||||
|  | 
 | ||||||
|  | 	React.useEffect(() => { | ||||||
|  | 		if (!loadedBlockedInstances ) { | ||||||
|  | 			Promise.try(() => { | ||||||
|  | 				return dispatch(api.admin.fetchDomainBlocks()); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}, []); | ||||||
|  | 
 | ||||||
|  | 	if (!loadedBlockedInstances) { | ||||||
|  | 		return ( | ||||||
|  | 			<div> | ||||||
|  | 				<h1>Federation</h1> | ||||||
|  | 				Loading... | ||||||
|  | 			</div> | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<Switch> | ||||||
|  | 			<Route path={`${base}/:domain`}> | ||||||
|  | 				<InstancePageWrapped /> | ||||||
|  | 			</Route> | ||||||
|  | 			<InstanceOverview /> | ||||||
|  | 		</Switch> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function InstanceOverview() { | ||||||
|  | 	const [filter, setFilter] = React.useState(""); | ||||||
|  | 	const blockedInstances = Redux.useSelector(state => state.admin.blockedInstances); | ||||||
|  | 	const [_location, setLocation] = useLocation(); | ||||||
|  | 
 | ||||||
|  | 	function filterFormSubmit(e) { | ||||||
|  | 		e.preventDefault(); | ||||||
|  | 		setLocation(`${base}/${filter}`); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<> | ||||||
|  | 			<h1>Federation</h1> | ||||||
|  | 			Here you can see an overview of blocked instances. | ||||||
|  | 
 | ||||||
|  | 			<div className="instance-list"> | ||||||
|  | 				<h2>Blocked instances</h2> | ||||||
|  | 				<form action={`${base}/view`} className="filter" role="search" onSubmit={filterFormSubmit}> | ||||||
|  | 					<input name="domain" value={filter} onChange={(e) => setFilter(e.target.value)}/> | ||||||
|  | 					<Link to={`${base}/${filter}`}><a className="button">Add block</a></Link> | ||||||
|  | 				</form> | ||||||
|  | 				<div className="list"> | ||||||
|  | 					{Object.values(blockedInstances).filter((a) => a.domain.startsWith(filter)).map((entry) => { | ||||||
|  | 						return ( | ||||||
|  | 							<Link key={entry.domain} to={`${base}/${entry.domain}`}> | ||||||
|  | 								<a className="entry nounderline"> | ||||||
|  | 									<span id="domain"> | ||||||
|  | 										{entry.domain} | ||||||
|  | 									</span> | ||||||
|  | 									<span id="date"> | ||||||
|  | 										{new Date(entry.created_at).toLocaleString()} | ||||||
|  | 									</span> | ||||||
|  | 								</a> | ||||||
|  | 							</Link> | ||||||
|  | 						); | ||||||
|  | 					})} | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<BulkBlocking/> | ||||||
|  | 		</> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const Bulk = formFields(adminActions.updateBulkBlockVal, (state) => state.admin.bulkBlock); | ||||||
|  | function BulkBlocking() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 	const {bulkBlock, blockedInstances} = Redux.useSelector(state => state.admin); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 	const [statusMsg, setStatus] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	function importBlocks() { | ||||||
|  | 		setStatus("Processing"); | ||||||
|  | 		setError(""); | ||||||
|  | 		return Promise.try(() => { | ||||||
|  | 			return dispatch(api.admin.bulkDomainBlock()); | ||||||
|  | 		}).then(({success, invalidDomains}) => { | ||||||
|  | 			return Promise.try(() => { | ||||||
|  | 				return resetBulk(); | ||||||
|  | 			}).then(() => { | ||||||
|  | 				dispatch(adminActions.updateBulkBlockVal(["list", invalidDomains.join("\n")])); | ||||||
|  | 
 | ||||||
|  | 				let stat = ""; | ||||||
|  | 				if (success == 0) { | ||||||
|  | 					return setError("No valid domains in import"); | ||||||
|  | 				} else if (success == 1) { | ||||||
|  | 					stat = "Imported 1 domain"; | ||||||
|  | 				} else { | ||||||
|  | 					stat = `Imported ${success} domains`; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if (invalidDomains.length > 0) { | ||||||
|  | 					if (invalidDomains.length == 1) { | ||||||
|  | 						stat += ", input contained 1 invalid domain."; | ||||||
|  | 					} else { | ||||||
|  | 						stat += `, input contained ${invalidDomains.length} invalid domains.`; | ||||||
|  | 					} | ||||||
|  | 				} else { | ||||||
|  | 					stat += "!"; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				setStatus(stat); | ||||||
|  | 			}); | ||||||
|  | 		}).catch((e) => { | ||||||
|  | 			console.error(e); | ||||||
|  | 			setError(e.message); | ||||||
|  | 			setStatus(""); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	function exportBlocks() { | ||||||
|  | 		return Promise.try(() => { | ||||||
|  | 			setStatus("Exporting"); | ||||||
|  | 			setError(""); | ||||||
|  | 			let asJSON = bulkBlock.exportType.startsWith("json"); | ||||||
|  | 			let _asCSV = bulkBlock.exportType.startsWith("csv"); | ||||||
|  | 
 | ||||||
|  | 			let exportList = Object.values(blockedInstances).map((entry) => { | ||||||
|  | 				if (asJSON) { | ||||||
|  | 					return { | ||||||
|  | 						domain: entry.domain, | ||||||
|  | 						public_comment: entry.public_comment | ||||||
|  | 					}; | ||||||
|  | 				} else { | ||||||
|  | 					return entry.domain; | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 			 | ||||||
|  | 			if (bulkBlock.exportType == "json") { | ||||||
|  | 				return dispatch(adminActions.updateBulkBlockVal(["list", JSON.stringify(exportList)])); | ||||||
|  | 			} else if (bulkBlock.exportType == "json-download") { | ||||||
|  | 				return fileDownload(JSON.stringify(exportList), "block-export.json"); | ||||||
|  | 			} else if (bulkBlock.exportType == "plain") { | ||||||
|  | 				return dispatch(adminActions.updateBulkBlockVal(["list", exportList.join("\n")])); | ||||||
|  | 			} | ||||||
|  | 		}).then(() => { | ||||||
|  | 			setStatus("Exported!"); | ||||||
|  | 		}).catch((e) => { | ||||||
|  | 			setError(e.message); | ||||||
|  | 			setStatus(""); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	function resetBulk(e) { | ||||||
|  | 		if (e != undefined) { | ||||||
|  | 			e.preventDefault(); | ||||||
|  | 		} | ||||||
|  | 		return dispatch(adminActions.resetBulkBlockVal()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	function disableInfoFields(props={}) { | ||||||
|  | 		if (bulkBlock.list[0] == "[") { | ||||||
|  | 			return { | ||||||
|  | 				...props, | ||||||
|  | 				disabled: true, | ||||||
|  | 				placeHolder: "Domain list is a JSON import, input disabled" | ||||||
|  | 			}; | ||||||
|  | 		} else { | ||||||
|  | 			return props; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<div className="bulk"> | ||||||
|  | 			<h2>Import / Export <a onClick={resetBulk}>reset</a></h2> | ||||||
|  | 			<Bulk.TextArea | ||||||
|  | 				id="list" | ||||||
|  | 				name="Domains, one per line" | ||||||
|  | 				placeHolder={`google.com\nfacebook.com`} | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<Bulk.TextArea | ||||||
|  | 				id="public_comment" | ||||||
|  | 				name="Public comment" | ||||||
|  | 				inputProps={disableInfoFields({rows: 3})} | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<Bulk.TextArea | ||||||
|  | 				id="private_comment" | ||||||
|  | 				name="Private comment" | ||||||
|  | 				inputProps={disableInfoFields({rows: 3})} | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<Bulk.Checkbox | ||||||
|  | 				id="obfuscate" | ||||||
|  | 				name="Obfuscate domains? " | ||||||
|  | 				inputProps={disableInfoFields()} | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<div className="hidden"> | ||||||
|  | 				<Bulk.File | ||||||
|  | 					id="json" | ||||||
|  | 					fileType="application/json" | ||||||
|  | 					withPreview={false} | ||||||
|  | 				/> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div className="messagebutton"> | ||||||
|  | 				<div> | ||||||
|  | 					<button type="submit" onClick={importBlocks}>Import</button> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div> | ||||||
|  | 					<button type="submit" onClick={exportBlocks}>Export</button> | ||||||
|  | 
 | ||||||
|  | 					<Bulk.Select id="exportType" name="Export type" options={ | ||||||
|  | 						<> | ||||||
|  | 							<option value="plain">One per line in text field</option> | ||||||
|  | 							<option value="json">JSON in text field</option> | ||||||
|  | 							<option value="json-download">JSON file download</option> | ||||||
|  | 							<option disabled value="csv">CSV in text field (glitch-soc)</option> | ||||||
|  | 							<option disabled value="csv-download">CSV file download (glitch-soc)</option> | ||||||
|  | 						</> | ||||||
|  | 					}/> | ||||||
|  | 				</div> | ||||||
|  | 				<br/> | ||||||
|  | 				<div> | ||||||
|  | 					{errorMsg.length > 0 &&  | ||||||
|  | 						<div className="error accent">{errorMsg}</div> | ||||||
|  | 					} | ||||||
|  | 					{statusMsg.length > 0 && | ||||||
|  | 						<div className="accent">{statusMsg}</div> | ||||||
|  | 					} | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function BackButton() { | ||||||
|  | 	return ( | ||||||
|  | 		<Link to={base}> | ||||||
|  | 			<a className="button">< back</a> | ||||||
|  | 		</Link> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function InstancePageWrapped() { | ||||||
|  | 	/* We wrap the component to generate formFields with a setter depending on the domain | ||||||
|  | 		 if formFields() is used inside the same component that is re-rendered with their state, | ||||||
|  | 		 inputs get re-created on every change, causing them to lose focus, and bad performance | ||||||
|  | 	*/ | ||||||
|  | 	let [_match, {domain}] = useRoute(`${base}/:domain`); | ||||||
|  | 
 | ||||||
|  | 	if (domain == "view") { // from form field submission
 | ||||||
|  | 		let realDomain = (new URL(document.location)).searchParams.get("domain"); | ||||||
|  | 		if (realDomain == undefined) { | ||||||
|  | 			return <Redirect to={base}/>; | ||||||
|  | 		} else { | ||||||
|  | 			domain = realDomain; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	function alterDomain([key, val]) { | ||||||
|  | 		return adminActions.updateDomainBlockVal([domain, key, val]); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const fields = formFields(alterDomain, (state) => state.admin.newInstanceBlocks[domain]); | ||||||
|  | 
 | ||||||
|  | 	return <InstancePage domain={domain} Form={fields} />; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function InstancePage({domain, Form}) { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 	const entry = Redux.useSelector(state => state.admin.newInstanceBlocks[domain]); | ||||||
|  | 	const [_location, setLocation] = useLocation(); | ||||||
|  | 
 | ||||||
|  | 	React.useEffect(() => { | ||||||
|  | 		if (entry == undefined) { | ||||||
|  | 			dispatch(api.admin.getEditableDomainBlock(domain)); | ||||||
|  | 		} | ||||||
|  | 	}, []); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 	const [statusMsg, setStatus] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	if (entry == undefined) { | ||||||
|  | 		return "Loading..."; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const updateBlock = submit( | ||||||
|  | 		() => dispatch(api.admin.updateDomainBlock(domain)), | ||||||
|  | 		{setStatus, setError} | ||||||
|  | 	); | ||||||
|  | 
 | ||||||
|  | 	const removeBlock = submit( | ||||||
|  | 		() => dispatch(api.admin.removeDomainBlock(domain)), | ||||||
|  | 		{setStatus, setError, startStatus: "Removing", successStatus: "Removed!", onSuccess: () => { | ||||||
|  | 			setLocation(base); | ||||||
|  | 		}} | ||||||
|  | 	); | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<div> | ||||||
|  | 			<h1><BackButton/> Federation settings for: {domain}</h1> | ||||||
|  | 			{entry.new && "No stored block yet, you can add one below:"} | ||||||
|  | 
 | ||||||
|  | 			<Form.TextArea | ||||||
|  | 				id="public_comment" | ||||||
|  | 				name="Public comment" | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<Form.TextArea | ||||||
|  | 				id="private_comment" | ||||||
|  | 				name="Private comment" | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<Form.Checkbox | ||||||
|  | 				id="obfuscate" | ||||||
|  | 				name="Obfuscate domain? " | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<div className="messagebutton"> | ||||||
|  | 				<button type="submit" onClick={updateBlock}>{entry.new ? "Add block" : "Save block"}</button> | ||||||
|  | 
 | ||||||
|  | 				{!entry.new && | ||||||
|  | 					<button className="danger" onClick={removeBlock}>Remove block</button> | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				{errorMsg.length > 0 &&  | ||||||
|  | 					<div className="error accent">{errorMsg}</div> | ||||||
|  | 				} | ||||||
|  | 				{statusMsg.length > 0 && | ||||||
|  | 					<div className="accent">{statusMsg}</div> | ||||||
|  | 				} | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										110
									
								
								web/source/settings-panel/admin/settings.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								web/source/settings-panel/admin/settings.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,110 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | 
 | ||||||
|  | const Submit = require("../components/submit"); | ||||||
|  | 
 | ||||||
|  | const api = require("../lib/api"); | ||||||
|  | const submit = require("../lib/submit"); | ||||||
|  | 
 | ||||||
|  | const adminActions = require("../redux/reducers/instances").actions; | ||||||
|  | 
 | ||||||
|  | const { | ||||||
|  | 	TextInput, | ||||||
|  | 	TextArea, | ||||||
|  | 	File | ||||||
|  | } = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings); | ||||||
|  | 
 | ||||||
|  | module.exports = function AdminSettings() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 	const [statusMsg, setStatus] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	const updateSettings = submit( | ||||||
|  | 		() => dispatch(api.admin.updateInstance()), | ||||||
|  | 		{setStatus, setError} | ||||||
|  | 	); | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<div> | ||||||
|  | 			<h1>Instance Settings</h1> | ||||||
|  | 			<TextInput | ||||||
|  | 				id="title" | ||||||
|  | 				name="Title" | ||||||
|  | 				placeHolder="My GoToSocial instance" | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<TextArea | ||||||
|  | 				id="short_description" | ||||||
|  | 				name="Short description" | ||||||
|  | 				placeHolder="A small testing instance for the GoToSocial alpha." | ||||||
|  | 			/> | ||||||
|  | 			<TextArea | ||||||
|  | 				id="description" | ||||||
|  | 				name="Full description" | ||||||
|  | 				placeHolder="A small testing instance for the GoToSocial alpha." | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<TextInput | ||||||
|  | 				id="contact_account.username" | ||||||
|  | 				name="Contact user (local account username)" | ||||||
|  | 				placeHolder="admin" | ||||||
|  | 			/> | ||||||
|  | 			<TextInput | ||||||
|  | 				id="email" | ||||||
|  | 				name="Contact email" | ||||||
|  | 				placeHolder="admin@example.com" | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			<TextArea | ||||||
|  | 				id="terms" | ||||||
|  | 				name="Terms & Conditions" | ||||||
|  | 				placeHolder="" | ||||||
|  | 			/> | ||||||
|  | 
 | ||||||
|  | 			{/* <div className="file-upload"> | ||||||
|  | 				<h3>Instance avatar</h3> | ||||||
|  | 				<div> | ||||||
|  | 					<img className="preview avatar" src={instance.avatar} alt={instance.avatar ? `Avatar image for the instance` : "No instance avatar image set"} /> | ||||||
|  | 					<File  | ||||||
|  | 						id="avatar" | ||||||
|  | 						fileType="image/*" | ||||||
|  | 					/> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div className="file-upload"> | ||||||
|  | 				<h3>Instance header</h3> | ||||||
|  | 				<div> | ||||||
|  | 					<img className="preview header" src={instance.header} alt={instance.header ? `Header image for the instance` : "No instance header image set"} /> | ||||||
|  | 					<File  | ||||||
|  | 						id="header" | ||||||
|  | 						fileType="image/*" | ||||||
|  | 					/> | ||||||
|  | 				</div> | ||||||
|  | 			</div> */} | ||||||
|  | 			<Submit onClick={updateSettings} label="Save" errorMsg={errorMsg} statusMsg={statusMsg} /> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
							
								
								
									
										45
									
								
								web/source/settings-panel/components/error.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								web/source/settings-panel/components/error.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | 
 | ||||||
|  | module.exports = function ErrorFallback({error, resetErrorBoundary}) { | ||||||
|  | 	return ( | ||||||
|  | 		<div className="error"> | ||||||
|  | 			<p> | ||||||
|  | 				{"An error occured, please report this on the "} | ||||||
|  | 				<a href="https://github.com/superseriousbusiness/gotosocial/issues">GoToSocial issue tracker</a> | ||||||
|  | 				{" or "} | ||||||
|  | 				<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>. | ||||||
|  | 				<br/>Include the details below: | ||||||
|  | 			</p> | ||||||
|  | 			<pre> | ||||||
|  | 				{error.name}: {error.message} | ||||||
|  | 			</pre> | ||||||
|  | 			<pre> | ||||||
|  | 				{error.stack} | ||||||
|  | 			</pre> | ||||||
|  | 			<p> | ||||||
|  | 				<button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a> | ||||||
|  | 			</p> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
							
								
								
									
										43
									
								
								web/source/settings-panel/components/fake-toot.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								web/source/settings-panel/components/fake-toot.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | 
 | ||||||
|  | module.exports = function FakeToot({children}) { | ||||||
|  | 	const account = Redux.useSelector((state) => state.user.profile); | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<div className="toot expanded"> | ||||||
|  | 			<div className="contentgrid"> | ||||||
|  | 				<span className="avatar"> | ||||||
|  | 					<img src={account.avatar} alt=""/> | ||||||
|  | 				</span> | ||||||
|  | 				<span className="displayname">{account.display_name.trim().length > 0 ? account.display_name : account.username}</span> | ||||||
|  | 				<span className="username">@{account.username}</span> | ||||||
|  | 				<div className="text"> | ||||||
|  | 					<div className="content"> | ||||||
|  | 						{children} | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
							
								
								
									
										167
									
								
								web/source/settings-panel/components/form-fields.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								web/source/settings-panel/components/form-fields.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,167 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | const d = require("dotty"); | ||||||
|  | const prettierBytes = require("prettier-bytes"); | ||||||
|  | 
 | ||||||
|  | function eventListeners(dispatch, setter, obj) { | ||||||
|  | 	return { | ||||||
|  | 		onTextChange: function (key) { | ||||||
|  | 			return function (e) { | ||||||
|  | 				dispatch(setter([key, e.target.value])); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 		 | ||||||
|  | 		onCheckChange: function (key) { | ||||||
|  | 			return function (e) { | ||||||
|  | 				dispatch(setter([key, e.target.checked])); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 		 | ||||||
|  | 		onFileChange: function (key, withPreview) { | ||||||
|  | 			return function (e) { | ||||||
|  | 				let file = e.target.files[0]; | ||||||
|  | 				if (withPreview) { | ||||||
|  | 					let old = d.get(obj, key); | ||||||
|  | 					if (old != undefined) { | ||||||
|  | 						URL.revokeObjectURL(old); // no error revoking a non-Object URL as provided by instance | ||||||
|  | 					} | ||||||
|  | 					let objectURL = URL.createObjectURL(file); | ||||||
|  | 					dispatch(setter([key, objectURL])); | ||||||
|  | 				} | ||||||
|  | 				dispatch(setter([`${key}File`, file])); | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function get(state, id, defaultVal) { | ||||||
|  | 	let value; | ||||||
|  | 	if (id.includes(".")) { | ||||||
|  | 		value = d.get(state, id); | ||||||
|  | 	} else { | ||||||
|  | 		value = state[id]; | ||||||
|  | 	} | ||||||
|  | 	if (value == undefined) { | ||||||
|  | 		value = defaultVal; | ||||||
|  | 	} | ||||||
|  | 	return value; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // function removeFile(name) { | ||||||
|  | // 	return function(e) { | ||||||
|  | // 		e.preventDefault(); | ||||||
|  | // 		dispatch(user.setProfileVal([name, ""])); | ||||||
|  | // 		dispatch(user.setProfileVal([`${name}File`, ""])); | ||||||
|  | // 	}; | ||||||
|  | // } | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  | 	formFields: function formFields(setter, selector) { | ||||||
|  | 		function FormField({ | ||||||
|  | 			type, id, name, className="", placeHolder="", fileType="", children=null, | ||||||
|  | 			options=null, inputProps={}, withPreview=true, showSize=false, maxSize=Infinity | ||||||
|  | 		}) { | ||||||
|  | 			const dispatch = Redux.useDispatch(); | ||||||
|  | 			let state = Redux.useSelector(selector); | ||||||
|  | 			let { | ||||||
|  | 				onTextChange, | ||||||
|  | 				onCheckChange, | ||||||
|  | 				onFileChange | ||||||
|  | 			} = eventListeners(dispatch, setter, state); | ||||||
|  | 
 | ||||||
|  | 			let field; | ||||||
|  | 			let defaultLabel = true; | ||||||
|  | 			if (type == "text") { | ||||||
|  | 				field = <input type="text" id={id} value={get(state, id, "")} placeholder={placeHolder} className={className} onChange={onTextChange(id)} {...inputProps}/>; | ||||||
|  | 			} else if (type == "textarea") { | ||||||
|  | 				field = <textarea type="text" id={id} value={get(state, id, "")} placeholder={placeHolder} className={className} onChange={onTextChange(id)} rows={8} {...inputProps}/>; | ||||||
|  | 			} else if (type == "checkbox") { | ||||||
|  | 				field = <input type="checkbox" id={id} checked={get(state, id, false)} className={className} onChange={onCheckChange(id)} {...inputProps}/>; | ||||||
|  | 			} else if (type == "select") { | ||||||
|  | 				field = ( | ||||||
|  | 					<select id={id} value={get(state, id, "")} className={className} onChange={onTextChange(id)} {...inputProps}> | ||||||
|  | 						{options} | ||||||
|  | 					</select> | ||||||
|  | 				); | ||||||
|  | 			} else if (type == "file") { | ||||||
|  | 				defaultLabel = false; | ||||||
|  | 				let file = get(state, `${id}File`); | ||||||
|  | 
 | ||||||
|  | 				let size = null; | ||||||
|  | 				if (showSize && file) { | ||||||
|  | 					size = `(${prettierBytes(file.size)})`; | ||||||
|  | 
 | ||||||
|  | 					if (file.size > maxSize) { | ||||||
|  | 						size = <span className="error-text">{size}</span>; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				field = ( | ||||||
|  | 					<> | ||||||
|  | 						<label htmlFor={id} className="file-input button">Browse</label> | ||||||
|  | 						<span> | ||||||
|  | 							{file ? file.name : "no file selected"} {size} | ||||||
|  | 						</span> | ||||||
|  | 						{/* <a onClick={removeFile("header")}>remove</a> */} | ||||||
|  | 						<input className="hidden" id={id} type="file" accept={fileType} onChange={onFileChange(id, withPreview)}  {...inputProps}/> | ||||||
|  | 					</> | ||||||
|  | 				); | ||||||
|  | 			} else { | ||||||
|  | 				defaultLabel = false; | ||||||
|  | 				field = `unsupported FormField ${type}, this is a developer error`; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			let label = <label htmlFor={id}>{name}</label>; | ||||||
|  | 			return ( | ||||||
|  | 				<div className={`form-field ${type}`}> | ||||||
|  | 					{defaultLabel ? label : null}	{field} | ||||||
|  | 					{children} | ||||||
|  | 				</div> | ||||||
|  | 			); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			TextInput: function(props) { | ||||||
|  | 				return <FormField type="text" {...props} />; | ||||||
|  | 			}, | ||||||
|  | 	 | ||||||
|  | 			TextArea: function(props) { | ||||||
|  | 				return <FormField type="textarea" {...props} />; | ||||||
|  | 			}, | ||||||
|  | 	 | ||||||
|  | 			Checkbox: function(props) { | ||||||
|  | 				return <FormField type="checkbox" {...props} />; | ||||||
|  | 			}, | ||||||
|  | 	 | ||||||
|  | 			Select: function(props) { | ||||||
|  | 				return <FormField type="select" {...props} />; | ||||||
|  | 			}, | ||||||
|  | 	 | ||||||
|  | 			File: function(props) { | ||||||
|  | 				return <FormField type="file" {...props} />; | ||||||
|  | 			}, | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	eventListeners | ||||||
|  | }; | ||||||
							
								
								
									
										102
									
								
								web/source/settings-panel/components/login.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								web/source/settings-panel/components/login.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |   You should have received a copy of the GNU Affero General Public License | ||||||
|  |   along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 	 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | 
 | ||||||
|  | const { setInstance } = require("../redux/reducers/oauth").actions; | ||||||
|  | const api = require("../lib/api"); | ||||||
|  | 
 | ||||||
|  | module.exports = function Login({error}) { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 	const [ instanceField, setInstanceField ] = React.useState(""); | ||||||
|  | 	const [ errorMsg, setErrorMsg ] = React.useState(); | ||||||
|  | 	const instanceFieldRef = React.useRef(""); | ||||||
|  | 
 | ||||||
|  | 	React.useEffect(() => { | ||||||
|  | 		// check if current domain runs an instance | ||||||
|  | 		let currentDomain = window.location.origin; | ||||||
|  | 		Promise.try(() => { | ||||||
|  | 			return dispatch(api.instance.fetchWithoutStore(currentDomain)); | ||||||
|  | 		}).then(() => { | ||||||
|  | 			if (instanceFieldRef.current.length == 0) { // user hasn't started typing yet | ||||||
|  | 				dispatch(setInstance(currentDomain)); | ||||||
|  | 				instanceFieldRef.current = currentDomain; | ||||||
|  | 				setInstanceField(currentDomain); | ||||||
|  | 			} | ||||||
|  | 		}).catch((e) => { | ||||||
|  | 			console.log("Current domain does not host a valid instance: ", e); | ||||||
|  | 		}); | ||||||
|  | 	}, []); | ||||||
|  | 
 | ||||||
|  | 	function tryInstance() { | ||||||
|  | 		let domain = instanceFieldRef.current; | ||||||
|  | 		Promise.try(() => { | ||||||
|  | 			return dispatch(api.instance.fetchWithoutStore(domain)).catch((e) => { | ||||||
|  | 				// TODO: clearer error messages for common errors | ||||||
|  | 				console.log(e); | ||||||
|  | 				throw e; | ||||||
|  | 			}); | ||||||
|  | 		}).then(() => { | ||||||
|  | 			dispatch(setInstance(domain)); | ||||||
|  | 
 | ||||||
|  | 			return dispatch(api.oauth.register()).catch((e) => { | ||||||
|  | 				console.log(e); | ||||||
|  | 				throw e; | ||||||
|  | 			}); | ||||||
|  | 		}).then(() => { | ||||||
|  | 			return dispatch(api.oauth.authorize()); // will send user off-page | ||||||
|  | 		}).catch((e) => { | ||||||
|  | 			setErrorMsg( | ||||||
|  | 				<> | ||||||
|  | 					<b>{e.type}</b> | ||||||
|  | 					<span>{e.message}</span> | ||||||
|  | 				</> | ||||||
|  | 			); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	function updateInstanceField(e) { | ||||||
|  | 		if (e.key == "Enter") { | ||||||
|  | 			tryInstance(instanceField); | ||||||
|  | 		} else { | ||||||
|  | 			setInstanceField(e.target.value); | ||||||
|  | 			instanceFieldRef.current = e.target.value; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<section className="login"> | ||||||
|  | 			<h1>OAUTH Login:</h1> | ||||||
|  | 			{error} | ||||||
|  | 			<form onSubmit={(e) => e.preventDefault()}> | ||||||
|  | 				<label htmlFor="instance">Instance: </label> | ||||||
|  | 				<input value={instanceField} onChange={updateInstanceField} id="instance"/> | ||||||
|  | 				{errorMsg &&  | ||||||
|  | 				<div className="error"> | ||||||
|  | 					{errorMsg} | ||||||
|  | 				</div> | ||||||
|  | 				} | ||||||
|  | 				<button onClick={tryInstance}>Authenticate</button> | ||||||
|  | 			</form> | ||||||
|  | 		</section> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
							
								
								
									
										33
									
								
								web/source/settings-panel/components/nav-button.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/source/settings-panel/components/nav-button.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const React = require("react"); | ||||||
|  | const { Link, useRoute } = require("wouter"); | ||||||
|  | 
 | ||||||
|  | module.exports = function NavButton({href, name}) { | ||||||
|  | 	const [isActive] = useRoute(`${href}/:anything?`); | ||||||
|  | 	return ( | ||||||
|  | 		<Link href={href}> | ||||||
|  | 			<a className={isActive ? "active" : ""} data-content={name}> | ||||||
|  | 				{name} | ||||||
|  | 			</a> | ||||||
|  | 		</Link> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
							
								
								
									
										35
									
								
								web/source/settings-panel/components/submit.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/source/settings-panel/components/submit.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const React = require("react"); | ||||||
|  | 
 | ||||||
|  | module.exports = function Submit({onClick, label, errorMsg, statusMsg}) { | ||||||
|  | 	return ( | ||||||
|  | 		<div className="messagebutton"> | ||||||
|  | 			<button type="submit" onClick={onClick}>{ label }</button> | ||||||
|  | 			{errorMsg.length > 0 &&  | ||||||
|  | 				<div className="error accent">{errorMsg}</div> | ||||||
|  | 			} | ||||||
|  | 			{statusMsg.length > 0 && | ||||||
|  | 				<div className="accent">{statusMsg}</div> | ||||||
|  | 			} | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
							
								
								
									
										178
									
								
								web/source/settings-panel/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								web/source/settings-panel/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,178 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | const ReactDom = require("react-dom/client"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | const { Switch, Route, Redirect } = require("wouter"); | ||||||
|  | const { Provider } = require("react-redux"); | ||||||
|  | const { PersistGate } = require("redux-persist/integration/react"); | ||||||
|  | 
 | ||||||
|  | const { store, persistor } = require("./redux"); | ||||||
|  | const api = require("./lib/api"); | ||||||
|  | const oauth = require("./redux/reducers/oauth").actions; | ||||||
|  | const { AuthenticationError } = require("./lib/errors"); | ||||||
|  | 
 | ||||||
|  | const Login = require("./components/login"); | ||||||
|  | 
 | ||||||
|  | require("./style.css"); | ||||||
|  | 
 | ||||||
|  | // TODO: nested categories?
 | ||||||
|  | const nav = { | ||||||
|  | 	"User": { | ||||||
|  | 		"Profile": require("./user/profile.js"), | ||||||
|  | 		"Settings": require("./user/settings.js"), | ||||||
|  | 	}, | ||||||
|  | 	"Admin": { | ||||||
|  | 		adminOnly: true, | ||||||
|  | 		"Instance Settings": require("./admin/settings.js"), | ||||||
|  | 		"Actions": require("./admin/actions"), | ||||||
|  | 		"Federation": require("./admin/federation.js"), | ||||||
|  | 		"Custom Emoji": require("./admin/emoji.js"), | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const { sidebar, panelRouter } = require("./lib/get-views")(nav); | ||||||
|  | 
 | ||||||
|  | function App() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 
 | ||||||
|  | 	const { loginState, isAdmin } = Redux.useSelector((state) => state.oauth); | ||||||
|  | 	const reduxTempStatus = Redux.useSelector((state) => state.temporary.status); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setErrorMsg] = React.useState(); | ||||||
|  | 	const [tokenChecked, setTokenChecked] = React.useState(false); | ||||||
|  | 
 | ||||||
|  | 	React.useEffect(() => { | ||||||
|  | 		if (loginState == "login" || loginState == "callback") { | ||||||
|  | 			Promise.try(() => { | ||||||
|  | 				// Process OAUTH authorization token from URL if available
 | ||||||
|  | 				if (loginState == "callback") { | ||||||
|  | 					let urlParams = new URLSearchParams(window.location.search); | ||||||
|  | 					let code = urlParams.get("code"); | ||||||
|  | 
 | ||||||
|  | 					if (code == undefined) { | ||||||
|  | 						setErrorMsg(new Error("Waiting for OAUTH callback but no ?code= provided. You can try logging in again:")); | ||||||
|  | 					} else { | ||||||
|  | 						return dispatch(api.oauth.tokenize(code)); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}).then(() => { | ||||||
|  | 				// Fetch current instance info
 | ||||||
|  | 				return dispatch(api.instance.fetch()); | ||||||
|  | 			}).then(() => { | ||||||
|  | 				// Check currently stored auth token for validity if available
 | ||||||
|  | 				return dispatch(api.user.fetchAccount()); | ||||||
|  | 			}).then(() => { | ||||||
|  | 				setTokenChecked(true); | ||||||
|  | 
 | ||||||
|  | 				return dispatch(api.oauth.checkIfAdmin()); | ||||||
|  | 			}).catch((e) => { | ||||||
|  | 				if (e instanceof AuthenticationError) { | ||||||
|  | 					dispatch(oauth.remove()); | ||||||
|  | 					e.message = "Stored OAUTH token no longer valid, please log in again."; | ||||||
|  | 				} | ||||||
|  | 				setErrorMsg(e); | ||||||
|  | 				console.error(e); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}, []); | ||||||
|  | 
 | ||||||
|  | 	let ErrorElement = null; | ||||||
|  | 	if (errorMsg != undefined) { | ||||||
|  | 		ErrorElement = ( | ||||||
|  | 			<div className="error"> | ||||||
|  | 				<b>{errorMsg.type}</b> | ||||||
|  | 				<span>{errorMsg.message}</span> | ||||||
|  | 			</div> | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const LogoutElement = ( | ||||||
|  | 		<button className="logout" onClick={() => { dispatch(api.oauth.logout()); }}> | ||||||
|  | 			Log out | ||||||
|  | 		</button> | ||||||
|  | 	); | ||||||
|  | 
 | ||||||
|  | 	if (reduxTempStatus != undefined) { | ||||||
|  | 		return ( | ||||||
|  | 			<section> | ||||||
|  | 				{reduxTempStatus} | ||||||
|  | 			</section> | ||||||
|  | 		); | ||||||
|  | 	} else if (tokenChecked && loginState == "login") { | ||||||
|  | 		return ( | ||||||
|  | 			<> | ||||||
|  | 				<div className="sidebar"> | ||||||
|  | 					{sidebar.all} | ||||||
|  | 					{isAdmin && sidebar.admin} | ||||||
|  | 					{LogoutElement} | ||||||
|  | 				</div> | ||||||
|  | 				<section className="with-sidebar"> | ||||||
|  | 					{ErrorElement} | ||||||
|  | 					<Switch> | ||||||
|  | 						{panelRouter.all} | ||||||
|  | 						{isAdmin && panelRouter.admin} | ||||||
|  | 						<Route> {/* default route */} | ||||||
|  | 							<Redirect to="/settings/user" /> | ||||||
|  | 						</Route> | ||||||
|  | 					</Switch> | ||||||
|  | 				</section> | ||||||
|  | 			</> | ||||||
|  | 		); | ||||||
|  | 	} else if (loginState == "none") { | ||||||
|  | 		return ( | ||||||
|  | 			<Login error={ErrorElement} /> | ||||||
|  | 		); | ||||||
|  | 	} else { | ||||||
|  | 		let status; | ||||||
|  | 
 | ||||||
|  | 		if (loginState == "login") { | ||||||
|  | 			status = "Verifying stored login..."; | ||||||
|  | 		} else if (loginState == "callback") { | ||||||
|  | 			status = "Processing OAUTH callback..."; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return ( | ||||||
|  | 			<section> | ||||||
|  | 				<div> | ||||||
|  | 					{status} | ||||||
|  | 				</div> | ||||||
|  | 				{ErrorElement} | ||||||
|  | 				{LogoutElement} | ||||||
|  | 			</section> | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function Main() { | ||||||
|  | 	return ( | ||||||
|  | 		<Provider store={store}> | ||||||
|  | 			<PersistGate loading={"loading..."} persistor={persistor}> | ||||||
|  | 				<App /> | ||||||
|  | 			</PersistGate> | ||||||
|  | 		</Provider> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const root = ReactDom.createRoot(document.getElementById("root")); | ||||||
|  | root.render(<React.StrictMode><Main /></React.StrictMode>); | ||||||
							
								
								
									
										192
									
								
								web/source/settings-panel/lib/api/admin.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								web/source/settings-panel/lib/api/admin.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,192 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const isValidDomain = require("is-valid-domain"); | ||||||
|  | 
 | ||||||
|  | const instance = require("../../redux/reducers/instances").actions; | ||||||
|  | const admin = require("../../redux/reducers/admin").actions; | ||||||
|  | 
 | ||||||
|  | module.exports = function ({ apiCall, getChanges }) { | ||||||
|  | 	const adminAPI = { | ||||||
|  | 		updateInstance: function updateInstance() { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					const state = getState().instances.adminSettings; | ||||||
|  | 
 | ||||||
|  | 					const update = getChanges(state, { | ||||||
|  | 						formKeys: ["title", "short_description", "description", "contact_account.username", "email", "terms"], | ||||||
|  | 						renamedKeys: {"contact_account.username": "contact_username"}, | ||||||
|  | 						// fileKeys: ["avatar", "header"]
 | ||||||
|  | 					}); | ||||||
|  | 
 | ||||||
|  | 					return dispatch(apiCall("PATCH", "/api/v1/instance", update, "form")); | ||||||
|  | 				}).then((data) => { | ||||||
|  | 					return dispatch(instance.setInstanceInfo(data)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		fetchDomainBlocks: function fetchDomainBlocks() { | ||||||
|  | 			return function (dispatch, _getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks")); | ||||||
|  | 				}).then((data) => { | ||||||
|  | 					return dispatch(admin.setBlockedInstances(data)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateDomainBlock: function updateDomainBlock(domain) { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					const state = getState().admin.newInstanceBlocks[domain]; | ||||||
|  | 					const update = getChanges(state, { | ||||||
|  | 						formKeys: ["domain", "obfuscate", "public_comment", "private_comment"], | ||||||
|  | 					}); | ||||||
|  | 
 | ||||||
|  | 					return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks", update, "form")); | ||||||
|  | 				}).then((block) => { | ||||||
|  | 					return Promise.all([ | ||||||
|  | 						dispatch(admin.newDomainBlock([domain, block])), | ||||||
|  | 						dispatch(admin.setDomainBlock([domain, block])) | ||||||
|  | 					]); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		getEditableDomainBlock: function getEditableDomainBlock(domain) { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				let data = getState().admin.blockedInstances[domain]; | ||||||
|  | 				return dispatch(admin.newDomainBlock([domain, data])); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		bulkDomainBlock: function bulkDomainBlock() { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				let invalidDomains = []; | ||||||
|  | 				let success = 0; | ||||||
|  | 
 | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					const state = getState().admin.bulkBlock; | ||||||
|  | 					let list = state.list; | ||||||
|  | 					let domains; | ||||||
|  | 
 | ||||||
|  | 					let fields = getChanges(state, { | ||||||
|  | 						formKeys: ["obfuscate", "public_comment", "private_comment"] | ||||||
|  | 					}); | ||||||
|  | 
 | ||||||
|  | 					let defaultDate = new Date().toUTCString(); | ||||||
|  | 					 | ||||||
|  | 					if (list[0] == "[") { | ||||||
|  | 						domains = JSON.parse(state.list); | ||||||
|  | 					} else { | ||||||
|  | 						domains = list.split("\n").map((line_) => { | ||||||
|  | 							let line = line_.trim(); | ||||||
|  | 							if (line.length == 0) { | ||||||
|  | 								return null; | ||||||
|  | 							} | ||||||
|  | 
 | ||||||
|  | 							if (!isValidDomain(line, {wildcard: true, allowUnicode: true})) { | ||||||
|  | 								invalidDomains.push(line); | ||||||
|  | 								return null; | ||||||
|  | 							} | ||||||
|  | 
 | ||||||
|  | 							return { | ||||||
|  | 								domain: line, | ||||||
|  | 								created_at: defaultDate, | ||||||
|  | 								...fields | ||||||
|  | 							}; | ||||||
|  | 						}).filter((a) => a != null); | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					if (domains.length == 0) { | ||||||
|  | 						return; | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					const update = { | ||||||
|  | 						domains: new Blob([JSON.stringify(domains)], {type: "application/json"}) | ||||||
|  | 					}; | ||||||
|  | 
 | ||||||
|  | 					return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks?import=true", update, "form")); | ||||||
|  | 				}).then((blocks) => { | ||||||
|  | 					if (blocks != undefined) { | ||||||
|  | 						return Promise.each(blocks, (block) => { | ||||||
|  | 							success += 1; | ||||||
|  | 							return dispatch(admin.setDomainBlock([block.domain, block])); | ||||||
|  | 						}); | ||||||
|  | 					} | ||||||
|  | 				}).then(() => { | ||||||
|  | 					return { | ||||||
|  | 						success, | ||||||
|  | 						invalidDomains | ||||||
|  | 					}; | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		removeDomainBlock: function removeDomainBlock(domain) { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					const id = getState().admin.blockedInstances[domain].id; | ||||||
|  | 					return dispatch(apiCall("DELETE", `/api/v1/admin/domain_blocks/${id}`)); | ||||||
|  | 				}).then((removed) => { | ||||||
|  | 					return dispatch(admin.removeDomainBlock(removed.domain)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		mediaCleanup: function mediaCleanup(days) { | ||||||
|  | 			return function (dispatch, _getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					return dispatch(apiCall("POST", `/api/v1/admin/media_cleanup?remote_cache_days=${days}`)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		fetchCustomEmoji: function fetchCustomEmoji() { | ||||||
|  | 			return function (dispatch, _getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					return dispatch(apiCall("GET", "/api/v1/custom_emojis")); | ||||||
|  | 				}).then((emoji) => { | ||||||
|  | 					return dispatch(admin.setEmoji(emoji)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		newEmoji: function newEmoji() { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					const state = getState().admin.newEmoji; | ||||||
|  | 
 | ||||||
|  | 					const update = getChanges(state, { | ||||||
|  | 						formKeys: ["shortcode"], | ||||||
|  | 						fileKeys: ["image"] | ||||||
|  | 					}); | ||||||
|  | 
 | ||||||
|  | 					return dispatch(apiCall("POST", "/api/v1/admin/custom_emojis", update, "form")); | ||||||
|  | 				}).then((emoji) => { | ||||||
|  | 					return dispatch(admin.addEmoji(emoji)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 	return adminAPI; | ||||||
|  | }; | ||||||
							
								
								
									
										185
									
								
								web/source/settings-panel/lib/api/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								web/source/settings-panel/lib/api/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,185 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const { isPlainObject } = require("is-plain-object"); | ||||||
|  | const d = require("dotty"); | ||||||
|  | 
 | ||||||
|  | const { APIError, AuthenticationError } = require("../errors"); | ||||||
|  | const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions; | ||||||
|  | const oauth = require("../../redux/reducers/oauth").actions; | ||||||
|  | 
 | ||||||
|  | function apiCall(method, route, payload, type = "json") { | ||||||
|  | 	return function (dispatch, getState) { | ||||||
|  | 		const state = getState(); | ||||||
|  | 		let base = state.oauth.instance; | ||||||
|  | 		let auth = state.oauth.token; | ||||||
|  | 		console.log(method, base, route, "auth:", auth != undefined); | ||||||
|  | 
 | ||||||
|  | 		return Promise.try(() => { | ||||||
|  | 			let url = new URL(base); | ||||||
|  | 			let [path, query] = route.split("?"); | ||||||
|  | 			url.pathname = path; | ||||||
|  | 			if (query != undefined) { | ||||||
|  | 				url.search = query; | ||||||
|  | 			} | ||||||
|  | 			let body; | ||||||
|  | 
 | ||||||
|  | 			let headers = { | ||||||
|  | 				"Accept": "application/json", | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			if (payload != undefined) { | ||||||
|  | 				if (type == "json") { | ||||||
|  | 					headers["Content-Type"] = "application/json"; | ||||||
|  | 					body = JSON.stringify(payload); | ||||||
|  | 				} else if (type == "form") { | ||||||
|  | 					const formData = new FormData(); | ||||||
|  | 					Object.entries(payload).forEach(([key, val]) => { | ||||||
|  | 						if (isPlainObject(val)) { | ||||||
|  | 							Object.entries(val).forEach(([key2, val2]) => { | ||||||
|  | 								if (val2 != undefined) { | ||||||
|  | 									formData.set(`${key}[${key2}]`, val2); | ||||||
|  | 								} | ||||||
|  | 							}); | ||||||
|  | 						} else { | ||||||
|  | 							if (val != undefined) { | ||||||
|  | 								formData.set(key, val); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}); | ||||||
|  | 					body = formData; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (auth != undefined) { | ||||||
|  | 				headers["Authorization"] = auth; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return fetch(url.toString(), { | ||||||
|  | 				method, | ||||||
|  | 				headers, | ||||||
|  | 				body | ||||||
|  | 			}); | ||||||
|  | 		}).then((res) => { | ||||||
|  | 			// try parse json even with error
 | ||||||
|  | 			let json = res.json().catch((e) => { | ||||||
|  | 				throw new APIError(`JSON parsing error: ${e.message}`); | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			return Promise.all([res, json]); | ||||||
|  | 		}).then(([res, json]) => { | ||||||
|  | 			if (!res.ok) { | ||||||
|  | 				if (auth != undefined && (res.status == 401 || res.status == 403)) { | ||||||
|  | 					// stored access token is invalid
 | ||||||
|  | 					throw new AuthenticationError("401: Authentication error", {json, status: res.status}); | ||||||
|  | 				} else { | ||||||
|  | 					throw new APIError(json.error, { json }); | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				return json; | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getChanges(state, keys) { | ||||||
|  | 	const { formKeys = [], fileKeys = [], renamedKeys = {} } = keys; | ||||||
|  | 	const update = {}; | ||||||
|  | 
 | ||||||
|  | 	formKeys.forEach((key) => { | ||||||
|  | 		let value = d.get(state, key); | ||||||
|  | 		if (value == undefined) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		if (renamedKeys[key]) { | ||||||
|  | 			key = renamedKeys[key]; | ||||||
|  | 		} | ||||||
|  | 		d.put(update, key, value); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	fileKeys.forEach((key) => { | ||||||
|  | 		let file = d.get(state, `${key}File`); | ||||||
|  | 		if (file != undefined) { | ||||||
|  | 			if (renamedKeys[key]) { | ||||||
|  | 				key = renamedKeys[key]; | ||||||
|  | 			} | ||||||
|  | 			d.put(update, key, file); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	return update; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getCurrentUrl() { | ||||||
|  | 	return `${window.location.origin}${window.location.pathname}`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function fetchInstanceWithoutStore(domain) { | ||||||
|  | 	return function (dispatch, getState) { | ||||||
|  | 		return Promise.try(() => { | ||||||
|  | 			let lookup = getState().instances.info[domain]; | ||||||
|  | 			if (lookup != undefined) { | ||||||
|  | 				return lookup; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// apiCall expects to pull the domain from state,
 | ||||||
|  | 			// but we don't want to store it there yet
 | ||||||
|  | 			// so we mock the API here with our function argument
 | ||||||
|  | 			let fakeState = { | ||||||
|  | 				oauth: { instance: domain } | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			return apiCall("GET", "/api/v1/instance")(dispatch, () => fakeState); | ||||||
|  | 		}).then((json) => { | ||||||
|  | 			if (json && json.uri) { // TODO: validate instance json more?
 | ||||||
|  | 				dispatch(setNamedInstanceInfo([domain, json])); | ||||||
|  | 				return json; | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function fetchInstance() { | ||||||
|  | 	return function (dispatch, _getState) { | ||||||
|  | 		return Promise.try(() => { | ||||||
|  | 			return dispatch(apiCall("GET", "/api/v1/instance")); | ||||||
|  | 		}).then((json) => { | ||||||
|  | 			if (json && json.uri) { | ||||||
|  | 				dispatch(setInstanceInfo(json)); | ||||||
|  | 				return json; | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let submoduleArgs = { apiCall, getCurrentUrl, getChanges }; | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  | 	instance: { | ||||||
|  | 		fetchWithoutStore: fetchInstanceWithoutStore, | ||||||
|  | 		fetch: fetchInstance | ||||||
|  | 	}, | ||||||
|  | 	oauth: require("./oauth")(submoduleArgs), | ||||||
|  | 	user: require("./user")(submoduleArgs), | ||||||
|  | 	admin: require("./admin")(submoduleArgs), | ||||||
|  | 	apiCall, | ||||||
|  | 	getChanges | ||||||
|  | }; | ||||||
							
								
								
									
										124
									
								
								web/source/settings-panel/lib/api/oauth.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								web/source/settings-panel/lib/api/oauth.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,124 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | 
 | ||||||
|  | const { OAUTHError, AuthenticationError } = require("../errors"); | ||||||
|  | 
 | ||||||
|  | const oauth = require("../../redux/reducers/oauth").actions; | ||||||
|  | const temporary = require("../../redux/reducers/temporary").actions; | ||||||
|  | const admin = require("../../redux/reducers/admin").actions; | ||||||
|  | 
 | ||||||
|  | module.exports = function oauthAPI({ apiCall, getCurrentUrl }) { | ||||||
|  | 	return { | ||||||
|  | 
 | ||||||
|  | 		register: function register(scopes = []) { | ||||||
|  | 			return function (dispatch, _getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					return dispatch(apiCall("POST", "/api/v1/apps", { | ||||||
|  | 						client_name: "GoToSocial Settings", | ||||||
|  | 						scopes: scopes.join(" "), | ||||||
|  | 						redirect_uris: getCurrentUrl(), | ||||||
|  | 						website: getCurrentUrl() | ||||||
|  | 					})); | ||||||
|  | 				}).then((json) => { | ||||||
|  | 					json.scopes = scopes; | ||||||
|  | 					dispatch(oauth.setRegistration(json)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		authorize: function authorize() { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				let state = getState(); | ||||||
|  | 				let reg = state.oauth.registration; | ||||||
|  | 				let base = new URL(state.oauth.instance); | ||||||
|  | 
 | ||||||
|  | 				base.pathname = "/oauth/authorize"; | ||||||
|  | 				base.searchParams.set("client_id", reg.client_id); | ||||||
|  | 				base.searchParams.set("redirect_uri", getCurrentUrl()); | ||||||
|  | 				base.searchParams.set("response_type", "code"); | ||||||
|  | 				base.searchParams.set("scope", reg.scopes.join(" ")); | ||||||
|  | 
 | ||||||
|  | 				dispatch(oauth.setLoginState("callback")); | ||||||
|  | 				dispatch(temporary.setStatus("Redirecting to instance login...")); | ||||||
|  | 
 | ||||||
|  | 				// send user to instance's login flow
 | ||||||
|  | 				window.location.assign(base.href); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		tokenize: function tokenize(code) { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				let reg = getState().oauth.registration; | ||||||
|  | 
 | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					if (reg == undefined || reg.client_id == undefined) { | ||||||
|  | 						throw new OAUTHError("Callback code present, but no client registration is available from localStorage. \nNote: localStorage is unavailable in Private Browsing."); | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					return dispatch(apiCall("POST", "/oauth/token", { | ||||||
|  | 						client_id: reg.client_id, | ||||||
|  | 						client_secret: reg.client_secret, | ||||||
|  | 						redirect_uri: getCurrentUrl(), | ||||||
|  | 						grant_type: "authorization_code", | ||||||
|  | 						code: code | ||||||
|  | 					})); | ||||||
|  | 				}).then((json) => { | ||||||
|  | 					window.history.replaceState({}, document.title, window.location.pathname); | ||||||
|  | 					return dispatch(oauth.login(json)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		checkIfAdmin: function checkIfAdmin() { | ||||||
|  | 			return function (dispatch, getState) { | ||||||
|  | 				const state = getState(); | ||||||
|  | 				let stored = state.oauth.isAdmin; | ||||||
|  | 				if (stored != undefined) { | ||||||
|  | 					return stored; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// newer GoToSocial version will include a `role` in the Account data, check that first
 | ||||||
|  | 				// TODO: check account data for admin status				
 | ||||||
|  | 
 | ||||||
|  | 				// no role info, try fetching an admin-only route and see if we get an error
 | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks")); | ||||||
|  | 				}).then((data) => { | ||||||
|  | 					return Promise.all([ | ||||||
|  | 						dispatch(oauth.setAdmin(true)), | ||||||
|  | 						dispatch(admin.setBlockedInstances(data)) | ||||||
|  | 					]); | ||||||
|  | 				}).catch(AuthenticationError, () => { | ||||||
|  | 					return dispatch(oauth.setAdmin(false)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		logout: function logout() { | ||||||
|  | 			return function (dispatch, _getState) { | ||||||
|  | 				// TODO: GoToSocial does not have a logout API route yet
 | ||||||
|  | 
 | ||||||
|  | 				return dispatch(oauth.remove()); | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
							
								
								
									
										67
									
								
								web/source/settings-panel/lib/api/user.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								web/source/settings-panel/lib/api/user.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,67 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | 
 | ||||||
|  | const user = require("../../redux/reducers/user").actions; | ||||||
|  | 
 | ||||||
|  | module.exports = function ({ apiCall, getChanges }) { | ||||||
|  | 	function updateCredentials(selector, keys) { | ||||||
|  | 		return function (dispatch, getState) { | ||||||
|  | 			return Promise.try(() => { | ||||||
|  | 				const state = selector(getState()); | ||||||
|  | 
 | ||||||
|  | 				const update = getChanges(state, keys); | ||||||
|  | 
 | ||||||
|  | 				return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form")); | ||||||
|  | 			}).then((account) => { | ||||||
|  | 				return dispatch(user.setAccount(account)); | ||||||
|  | 			}); | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return { | ||||||
|  | 		fetchAccount: function fetchAccount() { | ||||||
|  | 			return function (dispatch, _getState) { | ||||||
|  | 				return Promise.try(() => { | ||||||
|  | 					return dispatch(apiCall("GET", "/api/v1/accounts/verify_credentials")); | ||||||
|  | 				}).then((account) => { | ||||||
|  | 					return dispatch(user.setAccount(account)); | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateProfile: function updateProfile() { | ||||||
|  | 			const formKeys = ["display_name", "locked", "source", "custom_css", "source.note"]; | ||||||
|  | 			const renamedKeys = { | ||||||
|  | 				"source.note": "note" | ||||||
|  | 			}; | ||||||
|  | 			const fileKeys = ["header", "avatar"]; | ||||||
|  | 
 | ||||||
|  | 			return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateSettings: function updateProfile() { | ||||||
|  | 			const formKeys = ["source"]; | ||||||
|  | 
 | ||||||
|  | 			return updateCredentials((state) => state.user.settings, {formKeys}); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
							
								
								
									
										27
									
								
								web/source/settings-panel/lib/errors.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								web/source/settings-panel/lib/errors.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |   You should have received a copy of the GNU Affero General Public License | ||||||
|  |   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 	 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const createError = require("create-error"); | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  | 	APIError: createError("APIError"), | ||||||
|  | 	OAUTHError: createError("OAUTHError"), | ||||||
|  | 	AuthenticationError: createError("AuthenticationError"), | ||||||
|  | }; | ||||||
							
								
								
									
										102
									
								
								web/source/settings-panel/lib/get-views.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								web/source/settings-panel/lib/get-views.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | const { Link, Route, Switch, Redirect } = require("wouter"); | ||||||
|  | const { ErrorBoundary } = require("react-error-boundary"); | ||||||
|  | 
 | ||||||
|  | const ErrorFallback = require("../components/error"); | ||||||
|  | const NavButton = require("../components/nav-button"); | ||||||
|  | 
 | ||||||
|  | function urlSafe(str) { | ||||||
|  | 	return str.toLowerCase().replace(/\s+/g, "-"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = function getViews(struct) { | ||||||
|  | 	const sidebar = { | ||||||
|  | 		all: [], | ||||||
|  | 		admin: [], | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const panelRouter = { | ||||||
|  | 		all: [], | ||||||
|  | 		admin: [], | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	Object.entries(struct).forEach(([name, entries]) => { | ||||||
|  | 		let sidebarEl = sidebar.all; | ||||||
|  | 		let panelRouterEl = panelRouter.all; | ||||||
|  | 
 | ||||||
|  | 		if (entries.adminOnly) { | ||||||
|  | 			sidebarEl = sidebar.admin; | ||||||
|  | 			panelRouterEl = panelRouter.admin; | ||||||
|  | 			delete entries.adminOnly; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		let base = `/settings/${urlSafe(name)}`; | ||||||
|  | 
 | ||||||
|  | 		let links = []; | ||||||
|  | 
 | ||||||
|  | 		let firstRoute; | ||||||
|  | 
 | ||||||
|  | 		Object.entries(entries).forEach(([name, ViewComponent]) => { | ||||||
|  | 			let url = `${base}/${urlSafe(name)}`; | ||||||
|  | 
 | ||||||
|  | 			if (firstRoute == undefined) { | ||||||
|  | 				firstRoute = url; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			panelRouterEl.push(( | ||||||
|  | 				<Route path={`${url}/:page?`} key={url}> | ||||||
|  | 					<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { }}> | ||||||
|  | 						{/* FIXME: implement onReset */} | ||||||
|  | 						<ViewComponent /> | ||||||
|  | 					</ErrorBoundary> | ||||||
|  | 				</Route> | ||||||
|  | 			)); | ||||||
|  | 
 | ||||||
|  | 			links.push( | ||||||
|  | 				<NavButton key={url} href={url} name={name} /> | ||||||
|  | 			); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		panelRouterEl.push( | ||||||
|  | 			<Route key={base} path={base}> | ||||||
|  | 				<Redirect to={firstRoute} /> | ||||||
|  | 			</Route> | ||||||
|  | 		); | ||||||
|  | 
 | ||||||
|  | 		sidebarEl.push( | ||||||
|  | 			<React.Fragment key={name}> | ||||||
|  | 				<Link href={firstRoute}> | ||||||
|  | 					<a> | ||||||
|  | 						<h2>{name}</h2> | ||||||
|  | 					</a> | ||||||
|  | 				</Link> | ||||||
|  | 				<nav> | ||||||
|  | 					{links} | ||||||
|  | 				</nav> | ||||||
|  | 			</React.Fragment> | ||||||
|  | 		); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	return { sidebar, panelRouter }; | ||||||
|  | }; | ||||||
							
								
								
									
										48
									
								
								web/source/settings-panel/lib/submit.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								web/source/settings-panel/lib/submit.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | 
 | ||||||
|  | module.exports = function submit(func, { | ||||||
|  | 	setStatus, setError, | ||||||
|  | 	startStatus="PATCHing", successStatus="Saved!", | ||||||
|  | 	onSuccess, | ||||||
|  | 	onError | ||||||
|  | }) { | ||||||
|  | 	return function() { | ||||||
|  | 		setStatus(startStatus); | ||||||
|  | 		setError(""); | ||||||
|  | 		return Promise.try(() => { | ||||||
|  | 			return func(); | ||||||
|  | 		}).then(() => { | ||||||
|  | 			setStatus(successStatus); | ||||||
|  | 			if (onSuccess != undefined) { | ||||||
|  | 				return onSuccess(); | ||||||
|  | 			} | ||||||
|  | 		}).catch((e) => { | ||||||
|  | 			setError(e.message); | ||||||
|  | 			setStatus(""); | ||||||
|  | 			console.error(e); | ||||||
|  | 			if (onError != undefined) { | ||||||
|  | 				onError(e); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
							
								
								
									
										48
									
								
								web/source/settings-panel/redux/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								web/source/settings-panel/redux/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | /* | ||||||
|  | 	 GoToSocial | ||||||
|  | 	 Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	 This program is free software: you can redistribute it and/or modify | ||||||
|  | 	 it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	 the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	 (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	 This program is distributed in the hope that it will be useful, | ||||||
|  | 	 but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	 GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	 You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const { createStore, combineReducers, applyMiddleware } = require("redux"); | ||||||
|  | const { persistStore, persistReducer } = require("redux-persist"); | ||||||
|  | const thunk = require("redux-thunk").default; | ||||||
|  | const { composeWithDevTools } = require("redux-devtools-extension"); | ||||||
|  | 
 | ||||||
|  | const persistConfig = { | ||||||
|  | 	key: "gotosocial-settings", | ||||||
|  | 	storage: require("redux-persist/lib/storage").default, | ||||||
|  | 	stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel2").default, | ||||||
|  | 	whitelist: ["oauth"], | ||||||
|  | 	blacklist: ["temporary"] | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const combinedReducers = combineReducers({ | ||||||
|  | 	oauth: require("./reducers/oauth").reducer, | ||||||
|  | 	instances: require("./reducers/instances").reducer, | ||||||
|  | 	temporary: require("./reducers/temporary").reducer, | ||||||
|  | 	user: require("./reducers/user").reducer, | ||||||
|  | 	admin: require("./reducers/admin").reducer, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const persistedReducer = persistReducer(persistConfig, combinedReducers); | ||||||
|  | const composedEnhancer = composeWithDevTools(applyMiddleware(thunk)); | ||||||
|  | 
 | ||||||
|  | const store = createStore(persistedReducer, composedEnhancer); | ||||||
|  | const persistor = persistStore(store); | ||||||
|  | 
 | ||||||
|  | module.exports = { store, persistor }; | ||||||
							
								
								
									
										131
									
								
								web/source/settings-panel/redux/reducers/admin.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								web/source/settings-panel/redux/reducers/admin.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,131 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const { createSlice } = require("@reduxjs/toolkit"); | ||||||
|  | const defaultValue = require("default-value"); | ||||||
|  | 
 | ||||||
|  | function sortBlocks(blocks) { | ||||||
|  | 	return blocks.sort((a, b) => { // alphabetical sort
 | ||||||
|  | 		return a.domain.localeCompare(b.domain); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function emptyBlock() { | ||||||
|  | 	return { | ||||||
|  | 		public_comment: "", | ||||||
|  | 		private_comment: "", | ||||||
|  | 		obfuscate: false | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function emptyEmojiForm() { | ||||||
|  | 	return { | ||||||
|  | 		shortcode: "" | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = createSlice({ | ||||||
|  | 	name: "admin", | ||||||
|  | 	initialState: { | ||||||
|  | 		loadedBlockedInstances: false, | ||||||
|  | 		blockedInstances: undefined, | ||||||
|  | 		bulkBlock: { | ||||||
|  | 			list: "", | ||||||
|  | 			exportType: "plain", | ||||||
|  | 			...emptyBlock() | ||||||
|  | 		}, | ||||||
|  | 		newInstanceBlocks: {}, | ||||||
|  | 		emoji: {}, | ||||||
|  | 		newEmoji: emptyEmojiForm() | ||||||
|  | 	}, | ||||||
|  | 	reducers: { | ||||||
|  | 		setBlockedInstances: (state, { payload }) => { | ||||||
|  | 			state.blockedInstances = {}; | ||||||
|  | 			sortBlocks(payload).forEach((entry) => { | ||||||
|  | 				state.blockedInstances[entry.domain] = entry; | ||||||
|  | 			}); | ||||||
|  | 			state.loadedBlockedInstances = true; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		newDomainBlock: (state, { payload: [domain, data] }) => { | ||||||
|  | 			if (data == undefined) { | ||||||
|  | 				data = { | ||||||
|  | 					new: true, | ||||||
|  | 					domain, | ||||||
|  | 					...emptyBlock() | ||||||
|  | 				}; | ||||||
|  | 			} | ||||||
|  | 			state.newInstanceBlocks[domain] = data; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		setDomainBlock: (state, { payload: [domain, data = {}] }) => { | ||||||
|  | 			state.blockedInstances[domain] = data; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		removeDomainBlock: (state, {payload: domain}) => { | ||||||
|  | 			delete state.blockedInstances[domain]; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateDomainBlockVal: (state, { payload: [domain, key, val] }) => { | ||||||
|  | 			state.newInstanceBlocks[domain][key] = val; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateBulkBlockVal: (state, { payload: [key, val] }) => { | ||||||
|  | 			state.bulkBlock[key] = val; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		resetBulkBlockVal: (state, { _payload }) => { | ||||||
|  | 			state.bulkBlock = { | ||||||
|  | 				list: "", | ||||||
|  | 				exportType: "plain", | ||||||
|  | 				...emptyBlock() | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		exportToField: (state, { _payload }) => { | ||||||
|  | 			state.bulkBlock.list = Object.values(state.blockedInstances).map((entry) => { | ||||||
|  | 				return entry.domain; | ||||||
|  | 			}).join("\n"); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		setEmoji: (state, {payload}) => { | ||||||
|  | 			state.emoji = {}; | ||||||
|  | 			payload.forEach((emoji) => { | ||||||
|  | 				if (emoji.category == undefined) { | ||||||
|  | 					emoji.category = "Unsorted"; | ||||||
|  | 				} | ||||||
|  | 				state.emoji[emoji.category] = defaultValue(state.emoji[emoji.category], []); | ||||||
|  | 				state.emoji[emoji.category].push(emoji); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateNewEmojiVal: (state, { payload: [key, val] }) => { | ||||||
|  | 			state.newEmoji[key] = val; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		addEmoji: (state, {payload: emoji}) => { | ||||||
|  | 			if (emoji.category == undefined) { | ||||||
|  | 				emoji.category = "Unsorted"; | ||||||
|  | 			} | ||||||
|  | 			state.emoji[emoji.category] = defaultValue(state.emoji[emoji.category], []); | ||||||
|  | 			state.emoji[emoji.category].push(emoji); | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }); | ||||||
							
								
								
									
										42
									
								
								web/source/settings-panel/redux/reducers/instances.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/source/settings-panel/redux/reducers/instances.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const {createSlice} = require("@reduxjs/toolkit"); | ||||||
|  | const d = require("dotty"); | ||||||
|  | 
 | ||||||
|  | module.exports = createSlice({ | ||||||
|  | 	name: "instances", | ||||||
|  | 	initialState: { | ||||||
|  | 		info: {}, | ||||||
|  | 	}, | ||||||
|  | 	reducers: { | ||||||
|  | 		setNamedInstanceInfo: (state, {payload}) => { | ||||||
|  | 			let [key, info] = payload; | ||||||
|  | 			state.info[key] = info; | ||||||
|  | 		}, | ||||||
|  | 		setInstanceInfo: (state, {payload}) => { | ||||||
|  | 			state.current = payload; | ||||||
|  | 			state.adminSettings = payload; | ||||||
|  | 		}, | ||||||
|  | 		setAdminSettingsVal: (state, {payload: [key, val]}) => { | ||||||
|  | 			d.put(state.adminSettings, key, val); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
							
								
								
									
										52
									
								
								web/source/settings-panel/redux/reducers/oauth.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								web/source/settings-panel/redux/reducers/oauth.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const {createSlice} = require("@reduxjs/toolkit"); | ||||||
|  | 
 | ||||||
|  | module.exports = createSlice({ | ||||||
|  | 	name: "oauth", | ||||||
|  | 	initialState: { | ||||||
|  | 		loginState: 'none', | ||||||
|  | 	}, | ||||||
|  | 	reducers: { | ||||||
|  | 		setInstance: (state, {payload}) => { | ||||||
|  | 			state.instance = payload; | ||||||
|  | 		}, | ||||||
|  | 		setRegistration: (state, {payload}) => { | ||||||
|  | 			state.registration = payload; | ||||||
|  | 		}, | ||||||
|  | 		setLoginState: (state, {payload}) => { | ||||||
|  | 			state.loginState = payload; | ||||||
|  | 		}, | ||||||
|  | 		login: (state, {payload}) => { | ||||||
|  | 			state.token = `${payload.token_type} ${payload.access_token}`; | ||||||
|  | 			state.loginState = "login"; | ||||||
|  | 		}, | ||||||
|  | 		remove: (state, {_payload}) => { | ||||||
|  | 			delete state.token; | ||||||
|  | 			delete state.registration; | ||||||
|  | 			delete state.isAdmin; | ||||||
|  | 			state.loginState = "none"; | ||||||
|  | 		}, | ||||||
|  | 		setAdmin: (state, {payload}) => { | ||||||
|  | 			state.isAdmin = payload; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
							
								
								
									
										32
									
								
								web/source/settings-panel/redux/reducers/temporary.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								web/source/settings-panel/redux/reducers/temporary.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const {createSlice} = require("@reduxjs/toolkit"); | ||||||
|  | 
 | ||||||
|  | module.exports = createSlice({ | ||||||
|  | 	name: "temporary", | ||||||
|  | 	initialState: { | ||||||
|  | 	}, | ||||||
|  | 	reducers: { | ||||||
|  | 		setStatus: function(state, {payload}) { | ||||||
|  | 			state.status = payload; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
							
								
								
									
										51
									
								
								web/source/settings-panel/redux/reducers/user.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								web/source/settings-panel/redux/reducers/user.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const { createSlice } = require("@reduxjs/toolkit"); | ||||||
|  | const d = require("dotty"); | ||||||
|  | const defaultValue = require("default-value"); | ||||||
|  | 
 | ||||||
|  | module.exports = createSlice({ | ||||||
|  | 	name: "user", | ||||||
|  | 	initialState: { | ||||||
|  | 		profile: {}, | ||||||
|  | 		settings: {} | ||||||
|  | 	}, | ||||||
|  | 	reducers: { | ||||||
|  | 		setAccount: (state, { payload }) => { | ||||||
|  | 			payload.source = defaultValue(payload.source, {}); | ||||||
|  | 			payload.source.language = defaultValue(payload.source.language.toUpperCase(), "EN"); | ||||||
|  | 			payload.source.status_format = defaultValue(payload.source.status_format, "plain"); | ||||||
|  | 			payload.source.sensitive = defaultValue(payload.source.sensitive, false); | ||||||
|  | 
 | ||||||
|  | 			state.profile = payload; | ||||||
|  | 			// /user/settings only needs a copy of the 'source' obj
 | ||||||
|  | 			state.settings = { | ||||||
|  | 				source: payload.source | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 		setProfileVal: (state, { payload: [key, val] }) => { | ||||||
|  | 			d.put(state.profile, key, val); | ||||||
|  | 		}, | ||||||
|  | 		setSettingsVal: (state, { payload: [key, val] }) => { | ||||||
|  | 			d.put(state.settings, key, val); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
							
								
								
									
										498
									
								
								web/source/settings-panel/style.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										498
									
								
								web/source/settings-panel/style.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,498 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | body { | ||||||
|  | 	grid-template-rows: auto 1fr; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .content { | ||||||
|  | 	grid-column: 1 / span 3; /* stretch entire width, to fit panel + sidebar nav */ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | section { | ||||||
|  | 	grid-column: 2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #root { | ||||||
|  | 	display: grid; | ||||||
|  | 	grid-template-columns: 1fr 90ch 1fr; | ||||||
|  | 	width: 100vw; | ||||||
|  | 	max-width: 100vw; | ||||||
|  | 	box-sizing: border-box; | ||||||
|  | 
 | ||||||
|  | 	section.with-sidebar { | ||||||
|  | 		border-left: none; | ||||||
|  | 		border-top-left-radius: 0; | ||||||
|  | 		border-bottom-left-radius: 0; | ||||||
|  | 
 | ||||||
|  | 		& > div { | ||||||
|  | 			border-left: 0.2rem solid $border-accent; | ||||||
|  | 			padding-left: 0.4rem; | ||||||
|  | 			display: flex; | ||||||
|  | 			flex-direction: column; | ||||||
|  | 			gap: 0.5rem; | ||||||
|  | 			margin: 2rem 0; | ||||||
|  | 
 | ||||||
|  | 			h2 { | ||||||
|  | 				margin: 0; | ||||||
|  | 				margin-bottom: 0.5rem; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			&:only-child { | ||||||
|  | 				border-left: none; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			&:first-child { | ||||||
|  | 				margin-top: 0; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			&:last-child { | ||||||
|  | 				margin-bottom: 0; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.sidebar { | ||||||
|  | 		align-self: start; | ||||||
|  | 		justify-self: end; | ||||||
|  | 		background: $settings-nav-bg; | ||||||
|  | 		border: $boxshadow-border; | ||||||
|  | 		box-shadow: $boxshadow; | ||||||
|  | 		border-radius: $br; | ||||||
|  | 		border-top-right-radius: 0; | ||||||
|  | 		border-bottom-right-radius: 0; | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-direction: column; | ||||||
|  | 		min-width: 12rem; | ||||||
|  | 
 | ||||||
|  | 		a { | ||||||
|  | 			text-decoration: none; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		a:first-child h2 { | ||||||
|  | 			border-top-left-radius: $br; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		h2 { | ||||||
|  | 			margin: 0; | ||||||
|  | 			padding: 0.5rem; | ||||||
|  | 			font-size: 0.9rem; | ||||||
|  | 			font-weight: bold; | ||||||
|  | 			text-transform: uppercase; | ||||||
|  | 			color: $settings-nav-header-fg; | ||||||
|  | 			background: $settings-nav-header-bg; | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		nav { | ||||||
|  | 			display: flex; | ||||||
|  | 			flex-direction: column; | ||||||
|  | 
 | ||||||
|  | 			a { | ||||||
|  | 				padding: 1rem; | ||||||
|  | 				text-decoration: none; | ||||||
|  | 				transition: 0.1s; | ||||||
|  | 				color: $fg; | ||||||
|  | 	 | ||||||
|  | 				&:hover { | ||||||
|  | 					color: $settings-nav-fg-hover; | ||||||
|  | 					background: $settings-nav-bg-hover; | ||||||
|  | 				} | ||||||
|  | 				 | ||||||
|  | 				&.active { | ||||||
|  | 					color: $settings-nav-fg-active; | ||||||
|  | 					background: $settings-nav-bg-active; | ||||||
|  | 					font-weight: bold; | ||||||
|  | 					text-decoration: underline; | ||||||
|  | 				} | ||||||
|  | 	 | ||||||
|  | 				/* reserve space for bold version of the element, so .active doesn't | ||||||
|  | 					 change container size */ | ||||||
|  | 				&::after {  | ||||||
|  | 					font-weight: bold; | ||||||
|  | 					text-decoration: underline; | ||||||
|  | 					display: block; | ||||||
|  | 					content: attr(data-content); | ||||||
|  | 					height: 1px; | ||||||
|  | 					color: transparent; | ||||||
|  | 					overflow: hidden; | ||||||
|  | 					visibility: hidden; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		nav:last-child a:last-child { | ||||||
|  | 			border-bottom-left-radius: $br; | ||||||
|  | 			border-bottom: none; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .capitalize { | ||||||
|  | 	text-transform: capitalize; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | section { | ||||||
|  | 	margin-bottom: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input, select, textarea { | ||||||
|  | 	box-sizing: border-box; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .error { | ||||||
|  | 	color: $error-fg; | ||||||
|  | 	background: $error-bg; | ||||||
|  | 	border: 0.02rem solid $error-fg; | ||||||
|  | 	border-radius: $br; | ||||||
|  | 	font-weight: bold; | ||||||
|  | 	padding: 0.5rem; | ||||||
|  | 	white-space: pre-wrap; | ||||||
|  | 
 | ||||||
|  | 	a { | ||||||
|  | 		color: $error-link; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pre { | ||||||
|  | 		background: $bg; | ||||||
|  | 		color: $fg; | ||||||
|  | 		padding: 1rem; | ||||||
|  | 		overflow: auto; | ||||||
|  | 		margin: 0; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .hidden { | ||||||
|  | 	display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .messagebutton, .messagebutton > div { | ||||||
|  | 	display: flex; | ||||||
|  | 	align-items: center; | ||||||
|  | 	flex-wrap: wrap; | ||||||
|  | 
 | ||||||
|  | 	div.padded { | ||||||
|  | 		margin-left: 1rem; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	button, .button { | ||||||
|  | 		white-space: nowrap; | ||||||
|  | 		margin-right: 1rem; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .messagebutton > div { | ||||||
|  | 	button, .button { | ||||||
|  | 		margin-top: 1rem; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .notImplemented { | ||||||
|  | 	border: 2px solid rgb(70, 79, 88); | ||||||
|  | 	background: repeating-linear-gradient( | ||||||
|  | 		-45deg, | ||||||
|  | 		#525c66, | ||||||
|  | 		#525c66 10px, | ||||||
|  | 		rgb(70, 79, 88) 10px, | ||||||
|  | 		rgb(70, 79, 88) 20px | ||||||
|  | 	) !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | section.with-sidebar > div { | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: column; | ||||||
|  | 	gap: 1rem; | ||||||
|  | 
 | ||||||
|  | 	input, textarea { | ||||||
|  | 		width: 100%; | ||||||
|  | 		line-height: 1.5rem; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	input[type=checkbox] { | ||||||
|  | 		justify-self: start; | ||||||
|  | 		width: initial; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	input:read-only { | ||||||
|  | 		border: none; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	input:invalid { | ||||||
|  | 		border-color: red; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	textarea { | ||||||
|  | 		width: 100%; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	h1 { | ||||||
|  | 		margin-bottom: 0.5rem; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	.moreinfolink { | ||||||
|  | 		font-size: 0.9em; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	.labelinput .border { | ||||||
|  | 		border-radius: 0.2rem; | ||||||
|  | 		border: 0.15rem solid $border_accent; | ||||||
|  | 		padding: 0.3rem; | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-direction: column; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	.file-input.button { | ||||||
|  | 		display: inline-block; | ||||||
|  | 		font-size: 1rem; | ||||||
|  | 		font-weight: normal; | ||||||
|  | 		padding: 0.3rem 0.3rem; | ||||||
|  | 		align-self: flex-start; | ||||||
|  | 		margin-right: 0.2rem; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	.labelinput, .labelselect { | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-direction: column; | ||||||
|  | 		gap: 0.4rem; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	.labelcheckbox { | ||||||
|  | 		display: flex; | ||||||
|  | 		gap: 0.4rem; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	.titlesave { | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-wrap: wrap; | ||||||
|  | 		gap: 0.4rem; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .file-upload > div { | ||||||
|  | 	display: flex; | ||||||
|  | 	gap: 1rem; | ||||||
|  | 
 | ||||||
|  | 	img { | ||||||
|  | 		height: 8rem; | ||||||
|  | 		border: 0.2rem solid $border-accent; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	img.avatar { | ||||||
|  | 		width: 8rem; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	img.header { | ||||||
|  | 		width: 24rem; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .user-profile { | ||||||
|  | 	.overview { | ||||||
|  | 		display: grid; | ||||||
|  | 		grid-template-columns: 70% 30%; | ||||||
|  | 
 | ||||||
|  | 		.basic { | ||||||
|  | 			margin-top: -4.5rem; | ||||||
|  | 
 | ||||||
|  | 			.avatar { | ||||||
|  | 				height: 5rem; | ||||||
|  | 				width: 5rem; | ||||||
|  | 			} | ||||||
|  | 	 | ||||||
|  | 			.displayname { | ||||||
|  | 				font-size: 1.3rem; | ||||||
|  | 				padding-top: 0; | ||||||
|  | 				padding-bottom: 0; | ||||||
|  | 				margin-top: 0.7rem; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.files { | ||||||
|  | 			width: 100%; | ||||||
|  | 			margin: 1rem; | ||||||
|  | 			margin-right: 0; | ||||||
|  | 			display: flex; | ||||||
|  | 			flex-direction: column; | ||||||
|  | 			justify-content: center; | ||||||
|  | 
 | ||||||
|  | 			div.form-field { | ||||||
|  | 				width: 100%; | ||||||
|  | 				display: flex; | ||||||
|  | 
 | ||||||
|  | 				span { | ||||||
|  | 					flex: 1 1 auto; | ||||||
|  | 					overflow: hidden; | ||||||
|  | 					text-overflow: ellipsis; | ||||||
|  | 					white-space: nowrap; | ||||||
|  | 					padding: 0.3rem 0; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			h3 { | ||||||
|  | 				margin-top: 0; | ||||||
|  | 				margin-bottom: 0.5rem; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			div:first-child { | ||||||
|  | 				margin-bottom: 1rem; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			span { | ||||||
|  | 				font-style: italic; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .form-field label { | ||||||
|  | 	font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .list { | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: column; | ||||||
|  | 	margin-top: 0.5rem; | ||||||
|  | 	max-height: 40rem; | ||||||
|  | 	overflow: auto; | ||||||
|  | 
 | ||||||
|  | 	.entry { | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-wrap: wrap; | ||||||
|  | 		background: $settings-entry-bg; | ||||||
|  | 
 | ||||||
|  | 		&:hover { | ||||||
|  | 			background: $settings-entry-hover-bg; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .instance-list { | ||||||
|  | 	.filter { | ||||||
|  | 		display: flex; | ||||||
|  | 		gap: 0.5rem; | ||||||
|  | 
 | ||||||
|  | 		input { | ||||||
|  | 			width: auto; | ||||||
|  | 			flex: 1 1 auto; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.entry { | ||||||
|  | 		padding: 0.3rem; | ||||||
|  | 		margin: 0.2rem 0; | ||||||
|  | 
 | ||||||
|  | 		#domain { | ||||||
|  | 			flex: 1 1 auto; | ||||||
|  | 			overflow: hidden; | ||||||
|  | 			white-space: nowrap; | ||||||
|  | 			text-overflow: ellipsis; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .bulk h2 { | ||||||
|  | 	display: flex; | ||||||
|  | 	justify-content: space-between; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .emoji-list { | ||||||
|  | 	background: $settings-entry-bg; | ||||||
|  | 
 | ||||||
|  | 	.entry { | ||||||
|  | 		padding: 0.5rem; | ||||||
|  | 		flex-direction: column; | ||||||
|  | 
 | ||||||
|  | 		.emoji-group { | ||||||
|  | 			display: flex; | ||||||
|  | 	 | ||||||
|  | 			a { | ||||||
|  | 				border-radius: $br; | ||||||
|  | 				padding: 0.4rem; | ||||||
|  | 				line-height: 0; | ||||||
|  | 	 | ||||||
|  | 				img { | ||||||
|  | 					height: 2rem; | ||||||
|  | 					width: 2rem; | ||||||
|  | 					object-fit: contain; | ||||||
|  | 					vertical-align: middle; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				&:hover { | ||||||
|  | 					background: $settings-entry-hover-bg; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		&:hover { | ||||||
|  | 			background: inherit; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .toot { | ||||||
|  | 	padding-top: 0.5rem; | ||||||
|  | 	.contentgrid { | ||||||
|  | 		padding: 0 0.5rem; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media screen and (max-width: 100ch) { | ||||||
|  | 	#root { | ||||||
|  | 		padding: 1rem; | ||||||
|  | 		grid-template-columns: 100%; | ||||||
|  | 		grid-template-rows: auto auto; | ||||||
|  | 
 | ||||||
|  | 		.sidebar { | ||||||
|  | 			justify-self: auto; | ||||||
|  | 			margin-bottom: 2rem; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.sidebar, section.with-sidebar { | ||||||
|  | 			border-top-left-radius: $br; | ||||||
|  | 			border-top-right-radius: $br; | ||||||
|  | 			border-bottom-left-radius: $br; | ||||||
|  | 			border-bottom-right-radius: $br; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.sidebar a:first-child h2 { | ||||||
|  | 			border-top-right-radius: $br; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	section { | ||||||
|  | 		grid-column: 1; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.user-profile .overview { | ||||||
|  | 		grid-template-columns: 100%; | ||||||
|  | 		grid-template-rows: auto auto; | ||||||
|  | 
 | ||||||
|  | 		.files { | ||||||
|  | 			margin: 0; | ||||||
|  | 			margin-top: 1rem; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	main section { | ||||||
|  | 		padding: 0.75rem; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.instance-list .filter { | ||||||
|  | 		flex-direction: column; | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										113
									
								
								web/source/settings-panel/user/profile.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								web/source/settings-panel/user/profile.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,113 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | 
 | ||||||
|  | const Submit = require("../components/submit"); | ||||||
|  | 
 | ||||||
|  | const api = require("../lib/api"); | ||||||
|  | const user = require("../redux/reducers/user").actions; | ||||||
|  | const submit = require("../lib/submit"); | ||||||
|  | 
 | ||||||
|  | const { formFields } = require("../components/form-fields"); | ||||||
|  | 
 | ||||||
|  | const { | ||||||
|  | 	TextInput, | ||||||
|  | 	TextArea, | ||||||
|  | 	Checkbox, | ||||||
|  | 	File | ||||||
|  | } = formFields(user.setProfileVal, (state) => state.user.profile); | ||||||
|  | 
 | ||||||
|  | module.exports = function UserProfile() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 	const account = Redux.useSelector(state => state.user.profile); | ||||||
|  | 	const instance = Redux.useSelector(state => state.instances.current); | ||||||
|  | 
 | ||||||
|  | 	const allowCustomCSS = instance.configuration.accounts.allow_custom_css; | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 	const [statusMsg, setStatus] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	const saveProfile = submit( | ||||||
|  | 		() => dispatch(api.user.updateProfile()), | ||||||
|  | 		{setStatus, setError} | ||||||
|  | 	); | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<div className="user-profile"> | ||||||
|  | 			<h1>Profile</h1> | ||||||
|  | 			<div className="overview"> | ||||||
|  | 				<div className="profile"> | ||||||
|  | 					<div className="headerimage"> | ||||||
|  | 						<img className="headerpreview" src={account.header} alt={account.header ? `header image for ${account.username}` : "None set"} /> | ||||||
|  | 					</div> | ||||||
|  | 					<div className="basic"> | ||||||
|  | 						<div id="profile-basic-filler2"></div> | ||||||
|  | 						<span className="avatar"><img className="avatarpreview" src={account.avatar} alt={account.avatar ? `avatar image for ${account.username}` : "None set"} /></span> | ||||||
|  | 						<div className="displayname">{account.display_name.trim().length > 0 ? account.display_name : account.username}</div> | ||||||
|  | 						<div className="username"><span>@{account.username}</span></div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 				<div className="files"> | ||||||
|  | 					<div> | ||||||
|  | 						<h3>Header</h3> | ||||||
|  | 						<File  | ||||||
|  | 							id="header" | ||||||
|  | 							fileType="image/*" | ||||||
|  | 						/> | ||||||
|  | 					</div> | ||||||
|  | 					<div> | ||||||
|  | 						<h3>Avatar</h3> | ||||||
|  | 						<File  | ||||||
|  | 							id="avatar" | ||||||
|  | 							fileType="image/*" | ||||||
|  | 						/> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 			<TextInput | ||||||
|  | 				id="display_name" | ||||||
|  | 				name="Name" | ||||||
|  | 				placeHolder="A GoToSocial user" | ||||||
|  | 			/> | ||||||
|  | 			<TextArea | ||||||
|  | 				id="source.note" | ||||||
|  | 				name="Bio" | ||||||
|  | 				placeHolder="Just trying out GoToSocial, my pronouns are they/them and I like sloths." | ||||||
|  | 			/> | ||||||
|  | 			<Checkbox | ||||||
|  | 				id="locked" | ||||||
|  | 				name="Manually approve follow requests? " | ||||||
|  | 			/> | ||||||
|  | 			{ !allowCustomCSS ? null :   | ||||||
|  | 				<TextArea | ||||||
|  | 					id="custom_css" | ||||||
|  | 					name="Custom CSS" | ||||||
|  | 					className="monospace" | ||||||
|  | 				> | ||||||
|  | 					<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom profile CSS (opens in a new tab)</a> | ||||||
|  | 				</TextArea> | ||||||
|  | 			} | ||||||
|  | 			<Submit onClick={saveProfile} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg} /> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
							
								
								
									
										140
									
								
								web/source/settings-panel/user/settings.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								web/source/settings-panel/user/settings.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,140 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	This program is free software: you can redistribute it and/or modify | ||||||
|  | 	it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	(at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	This program is distributed in the hope that it will be useful, | ||||||
|  | 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const Promise = require("bluebird"); | ||||||
|  | const React = require("react"); | ||||||
|  | const Redux = require("react-redux"); | ||||||
|  | 
 | ||||||
|  | const api = require("../lib/api"); | ||||||
|  | const user = require("../redux/reducers/user").actions; | ||||||
|  | const submit = require("../lib/submit"); | ||||||
|  | 
 | ||||||
|  | const Languages = require("../components/languages"); | ||||||
|  | const Submit = require("../components/submit"); | ||||||
|  | 
 | ||||||
|  | const { | ||||||
|  | 	Checkbox, | ||||||
|  | 	Select, | ||||||
|  | } = require("../components/form-fields").formFields(user.setSettingsVal, (state) => state.user.settings); | ||||||
|  | 
 | ||||||
|  | module.exports = function UserSettings() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 	const [statusMsg, setStatus] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	const updateSettings = submit( | ||||||
|  | 		() => dispatch(api.user.updateSettings()), | ||||||
|  | 		{setStatus, setError} | ||||||
|  | 	); | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<> | ||||||
|  | 			<div className="user-settings"> | ||||||
|  | 				<h1>Post settings</h1> | ||||||
|  | 				<Select id="source.language" name="Default post language" options={ | ||||||
|  | 					<Languages/> | ||||||
|  | 				}> | ||||||
|  | 				</Select> | ||||||
|  | 				<Select id="source.privacy" name="Default post privacy" options={ | ||||||
|  | 					<> | ||||||
|  | 						<option value="private">Private / followers-only</option> | ||||||
|  | 						<option value="unlisted">Unlisted</option> | ||||||
|  | 						<option value="public">Public</option> | ||||||
|  | 					</> | ||||||
|  | 				}> | ||||||
|  | 					<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a> | ||||||
|  | 				</Select> | ||||||
|  | 				<Select id="source.status_format" name="Default post format" options={ | ||||||
|  | 					<> | ||||||
|  | 						<option value="plain">Plain (default)</option> | ||||||
|  | 						<option value="markdown">Markdown</option> | ||||||
|  | 					</> | ||||||
|  | 				}> | ||||||
|  | 					<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a> | ||||||
|  | 				</Select> | ||||||
|  | 				<Checkbox | ||||||
|  | 					id="source.sensitive" | ||||||
|  | 					name="Mark my posts as sensitive by default" | ||||||
|  | 				/> | ||||||
|  | 
 | ||||||
|  | 				<Submit onClick={updateSettings} label="Save post settings" errorMsg={errorMsg} statusMsg={statusMsg}/> | ||||||
|  | 			</div> | ||||||
|  | 			<div> | ||||||
|  | 				<PasswordChange/> | ||||||
|  | 			</div> | ||||||
|  | 		</> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function PasswordChange() { | ||||||
|  | 	const dispatch = Redux.useDispatch(); | ||||||
|  | 
 | ||||||
|  | 	const [errorMsg, setError] = React.useState(""); | ||||||
|  | 	const [statusMsg, setStatus] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	const [oldPassword, setOldPassword] = React.useState(""); | ||||||
|  | 	const [newPassword, setNewPassword] = React.useState(""); | ||||||
|  | 	const [newPasswordConfirm, setNewPasswordConfirm] = React.useState(""); | ||||||
|  | 
 | ||||||
|  | 	function changePassword() { | ||||||
|  | 		if (newPassword !== newPasswordConfirm) { | ||||||
|  | 			setError("New password and confirm new password did not match!"); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		setStatus("PATCHing"); | ||||||
|  | 		setError(""); | ||||||
|  | 		return Promise.try(() => { | ||||||
|  | 			let data = { | ||||||
|  | 				old_password: oldPassword, | ||||||
|  | 				new_password: newPassword | ||||||
|  | 			}; | ||||||
|  | 			return dispatch(api.apiCall("POST", "/api/v1/user/password_change", data, "form")); | ||||||
|  | 		}).then(() => { | ||||||
|  | 			setStatus("Saved!"); | ||||||
|  | 			setOldPassword(""); | ||||||
|  | 			setNewPassword(""); | ||||||
|  | 			setNewPasswordConfirm(""); | ||||||
|  | 		}).catch((e) => { | ||||||
|  | 			setError(e.message); | ||||||
|  | 			setStatus(""); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<> | ||||||
|  | 			<h1>Change password</h1> | ||||||
|  | 			<div className="labelinput"> | ||||||
|  | 				<label htmlFor="password">Current password</label> | ||||||
|  | 				<input name="password" id="password" type="password" autoComplete="current-password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} /> | ||||||
|  | 			</div> | ||||||
|  | 			<div className="labelinput"> | ||||||
|  | 				<label htmlFor="new-password">New password</label> | ||||||
|  | 				<input name="new-password" id="new-password" type="password" autoComplete="new-password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} /> | ||||||
|  | 			</div> | ||||||
|  | 			<div className="labelinput"> | ||||||
|  | 				<label htmlFor="confirm-new-password">Confirm new password</label> | ||||||
|  | 				<input name="confirm-new-password" id="confirm-new-password" type="password" autoComplete="new-password" value={newPasswordConfirm} onChange={(e) => setNewPasswordConfirm(e.target.value)} /> | ||||||
|  | 			</div> | ||||||
|  | 			<Submit onClick={changePassword} label="Save new password" errorMsg={errorMsg} statusMsg={statusMsg}/> | ||||||
|  | 		</> | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										1516
									
								
								web/source/yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										1516
									
								
								web/source/yarn.lock
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1,5 +1,6 @@ | ||||||
| {{ template "header.tmpl" .}} | {{ template "header.tmpl" .}} | ||||||
| <main class="lightgray"> | <main class="lightgray"> | ||||||
| 	<div id="root"></div> | 	<div id="root"> | ||||||
|  | 	</div> | ||||||
| </main> | </main> | ||||||
| {{ template "footer.tmpl" .}} | {{ template "footer.tmpl" .}} | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| {{ template "header.tmpl" .}} | {{ template "header.tmpl" .}} | ||||||
| <section class="excerpt_top"> | <section class="excerpt-top"> | ||||||
| 	home to <span class="count">{{.instance.Stats.user_count}}</span> users | 	home to <span class="count">{{.instance.Stats.user_count}}</span> users | ||||||
| 		who posted <span class="count">{{.instance.Stats.status_count}}</span> statuses, | 		who posted <span class="count">{{.instance.Stats.status_count}}</span> statuses, | ||||||
| 		federating with  <span class="count">{{.instance.Stats.domain_count}}</span> other instances. | 		federating with  <span class="count">{{.instance.Stats.domain_count}}</span> other instances. | ||||||
|  |  | ||||||
|  | @ -2,6 +2,10 @@ | ||||||
| 	<a href="{{.Account.URL}}" class="avatar"><img src="{{.Account.Avatar}}" alt=""></a> | 	<a href="{{.Account.URL}}" class="avatar"><img src="{{.Account.Avatar}}" alt=""></a> | ||||||
| 	<a href="{{.Account.URL}}" class="displayname">{{if .Account.DisplayName}}{{emojify .Account.Emojis (escape .Account.DisplayName)}}{{else}}{{.Account.Username}}{{end}}</a> | 	<a href="{{.Account.URL}}" class="displayname">{{if .Account.DisplayName}}{{emojify .Account.Emojis (escape .Account.DisplayName)}}{{else}}{{.Account.Username}}{{end}}</a> | ||||||
| 	<a href="{{.Account.URL}}" class="username">@{{.Account.Acct}}</a> | 	<a href="{{.Account.URL}}" class="username">@{{.Account.Acct}}</a> | ||||||
|  | 	<div class="not-expanded"> | ||||||
|  | 		<span class="visibility">{{.Visibility | visibilityIcon}}</span> | ||||||
|  | 		<span class="date">{{.CreatedAt | timestamp}}</span> | ||||||
|  | 	</div> | ||||||
| 	<div class="text"> | 	<div class="text"> | ||||||
| 		{{if .SpoilerText}} | 		{{if .SpoilerText}} | ||||||
| 		<input class="spoiler" id="hideSpoiler-{{.ID}}" type="checkbox" style="display: none" aria-hidden="true" checked="true" /> | 		<input class="spoiler" id="hideSpoiler-{{.ID}}" type="checkbox" style="display: none" aria-hidden="true" checked="true" /> | ||||||
|  | @ -43,7 +47,6 @@ | ||||||
| <div class="info"> | <div class="info"> | ||||||
| 	<div id="date">{{.CreatedAt | timestamp}}</div> | 	<div id="date">{{.CreatedAt | timestamp}}</div> | ||||||
| 	<div class="stats"> | 	<div class="stats"> | ||||||
| 		<div id="visibility">{{.Visibility | visibilityIcon}}</div> |  | ||||||
| 		<div id="replies"><i aria-label="Replies" class="fa fa-reply-all"></i> {{.RepliesCount}}</div> | 		<div id="replies"><i aria-label="Replies" class="fa fa-reply-all"></i> {{.RepliesCount}}</div> | ||||||
| 		<div id="boosts"><i aria-label="Boosts" class="fa fa-retweet"></i> {{.ReblogsCount}}</div> | 		<div id="boosts"><i aria-label="Boosts" class="fa fa-retweet"></i> {{.ReblogsCount}}</div> | ||||||
| 		<div id="favorites"><i aria-label="Favorites" class="fa fa-star"></i> {{.FavouritesCount}}</div> | 		<div id="favorites"><i aria-label="Favorites" class="fa fa-star"></i> {{.FavouritesCount}}</div> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue