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