federating my verse (iceshrimp & mastodon API compat, read-only)
This commit is contained in:
		
							parent
							
								
									8dc8190cdf
								
							
						
					
					
						commit
						da93978820
					
				
					 67 changed files with 2743 additions and 649 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | **/.DS_Store | ||||||
|  | node_modules/ | ||||||
|  | dist/ | ||||||
							
								
								
									
										13
									
								
								index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  |     <head> | ||||||
|  |         <meta charset="UTF-8"> | ||||||
|  |         <link rel="icon" type="image/png" href="/favicon.png"> | ||||||
|  |         <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  |         <title>space social</title> | ||||||
|  |     </head> | ||||||
|  |     <body> | ||||||
|  |         <div id="app"></div> | ||||||
|  |         <script type="module" src="/src/main.js"></script> | ||||||
|  |     </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										1185
									
								
								package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1185
									
								
								package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										18
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | { | ||||||
|  |     "name": "spacesocial-client", | ||||||
|  |     "version": "0.1.0", | ||||||
|  |     "description": "social media for the galaxy-wide-web! 🌌", | ||||||
|  |     "type": "module", | ||||||
|  |     "scripts": { | ||||||
|  |         "dev": "vite", | ||||||
|  |         "build": "vite build", | ||||||
|  |         "preview": "vite preview" | ||||||
|  |     }, | ||||||
|  |     "author": "", | ||||||
|  |     "license": "ISC", | ||||||
|  |     "devDependencies": { | ||||||
|  |         "@sveltejs/vite-plugin-svelte": "^3.1.1", | ||||||
|  |         "svelte": "^4.2.18", | ||||||
|  |         "vite": "^5.3.1" | ||||||
|  |     } | ||||||
|  | } | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 88 KiB | 
|  | @ -1,240 +0,0 @@ | ||||||
| :root { |  | ||||||
|     --fg0: #eee; |  | ||||||
|     --bg0: #080808; |  | ||||||
|     --bg1: #101010; |  | ||||||
|     --bg2: #121212; |  | ||||||
|     --accent: #b7fd49; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| body { |  | ||||||
|     margin: 0; |  | ||||||
|     padding: 0; |  | ||||||
|      |  | ||||||
|     background-color: var(--bg0); |  | ||||||
|     color: var(--fg0); |  | ||||||
| 
 |  | ||||||
|     font-family: "Inter", sans-serif; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #feed { |  | ||||||
|     width: 720px; |  | ||||||
|     margin: 0 auto; |  | ||||||
|     background-color: var(--bg1); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-container { |  | ||||||
|     padding: 20px 32px; |  | ||||||
|     border-bottom: 1px solid #8884; |  | ||||||
|     transition: background-color .1s; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-container:hover { |  | ||||||
|     background-color: var(--bg2); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-context { |  | ||||||
|     margin-bottom: 8px; |  | ||||||
|     padding-left: 58px; |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: row; |  | ||||||
|     align-items: center; |  | ||||||
|     color: var(--accent); |  | ||||||
|     opacity: .8; |  | ||||||
|     transition: opacity .1s; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-container:hover .post-context { |  | ||||||
|     opacity: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-context-icon { |  | ||||||
|     margin-right: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-context a, |  | ||||||
| .post-context a:visited, |  | ||||||
| .post-header-container a, |  | ||||||
| .post-header-container a:visited { |  | ||||||
|     color: inherit; |  | ||||||
|     text-decoration: none; |  | ||||||
| } |  | ||||||
| .post-context a:hover, |  | ||||||
| .post-header-container a:hover { |  | ||||||
|     text-decoration: underline; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-context-time { |  | ||||||
|     margin-left: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| article.post { /* ... */ } |  | ||||||
| 
 |  | ||||||
| .post-header-container { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: row; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-avatar-container { |  | ||||||
|     margin-right: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-avatar { |  | ||||||
|     border-radius: 8px; |  | ||||||
|     box-shadow: 2px 2px #0004; |  | ||||||
|     /* transition: transform .2s ease-out; */ |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* .post-avatar:hover { */ |  | ||||||
| /*     transform: scale(1.1); */ |  | ||||||
| /* } */ |  | ||||||
| 
 |  | ||||||
| .post-header { |  | ||||||
|     display: flex; |  | ||||||
|     flex-grow: 1; |  | ||||||
|     flex-direction: row; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-info { |  | ||||||
|     margin-left: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-user-info a { |  | ||||||
|     display: block; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-body { |  | ||||||
|     margin-top: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container { |  | ||||||
|     margin-top: 8px; |  | ||||||
|     display: grid; |  | ||||||
|     grid-gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container[data-count="1"] { |  | ||||||
|     grid-template-rows: 1fr; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container[data-count="2"] { |  | ||||||
|     grid-template-columns: 1fr 1fr; |  | ||||||
|     grid-template-rows: 1fr; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container[data-count="3"] { |  | ||||||
|     grid-template-columns: 1fr .5fr; |  | ||||||
|     grid-template-rows: 1fr 1fr; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container[data-count="4"] { |  | ||||||
|     grid-template-columns: 1fr 1fr; |  | ||||||
|     grid-template-rows: 1fr 1fr; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media { |  | ||||||
|     border-radius: 12px; |  | ||||||
|     background-color: #000; |  | ||||||
|     overflow: hidden; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media a { |  | ||||||
|     width: 100%; |  | ||||||
|     height: 100%; |  | ||||||
|     display: block; |  | ||||||
|     cursor: zoom-in; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media a img { |  | ||||||
|     width: 100%; |  | ||||||
|     height: 100%; |  | ||||||
|     display: block; |  | ||||||
|     object-fit: contain; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container > :nth-child(1) { |  | ||||||
|     grid-column: 1/2; |  | ||||||
|     grid-row: 1/2; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container[data-count="3"] > :nth-child(1) { |  | ||||||
|     grid-row: 1/3; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container > :nth-child(2) { |  | ||||||
|     grid-column: 2/2; |  | ||||||
|     grid-row: 1/2; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container > :nth-child(3) { |  | ||||||
|     grid-column: 1/2; |  | ||||||
|     grid-row: 2/2; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container[data-count="3"] > :nth-child(3) { |  | ||||||
|     grid-column: 2/2; |  | ||||||
|     grid-row: 2/2; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-media-container > :nth-child(4) { |  | ||||||
|     grid-column: 2/2; |  | ||||||
|     grid-row: 2/2; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-container footer { |  | ||||||
|     opacity: .8; |  | ||||||
|     transition: opacity .1s; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-container:hover footer { |  | ||||||
|     opacity: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-reactions { |  | ||||||
|     margin-top: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| button.reaction { |  | ||||||
|     padding: 6px 8px; |  | ||||||
|     font-size: 1em; |  | ||||||
|     background: none; |  | ||||||
|     color: inherit; |  | ||||||
|     border: none; |  | ||||||
|     border-radius: 8px; |  | ||||||
|     /* transition: transform .1s ease-out; */ |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| button.reaction:hover, |  | ||||||
| .post-actions button:hover { |  | ||||||
|     /* transform: scale(1.1); */ |  | ||||||
|     background: #8881; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| button.reaction:active, |  | ||||||
| .post-actions button:active { |  | ||||||
|     /* transform: scale(.95); */ |  | ||||||
|     background: #0001; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| button.reaction.active, |  | ||||||
| .post-actions button.active { |  | ||||||
|     background: var(--accent); |  | ||||||
|     color: var(--bg0); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-actions { |  | ||||||
|     margin-top: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-actions button { |  | ||||||
|     padding: 6px 8px; |  | ||||||
|     font-size: 1em; |  | ||||||
|     background: none; |  | ||||||
|     color: inherit; |  | ||||||
|     border: none; |  | ||||||
|     border-radius: 8px; |  | ||||||
|     transition: transform .1s ease-out; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .post-actions button .count { |  | ||||||
|     opacity: .5; |  | ||||||
| } |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-Black.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-Black.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-BlackItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-BlackItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-Bold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-Bold.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-BoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-BoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-ExtraBold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-ExtraBold.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-ExtraBoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-ExtraBoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-ExtraLight.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-ExtraLight.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-ExtraLightItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-ExtraLightItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-Light.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-Light.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-LightItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-LightItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-Medium.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-Medium.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-MediumItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-MediumItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-SemiBold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-SemiBold.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-SemiBoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-SemiBoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-Thin.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-Thin.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/Inter-ThinItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/Inter-ThinItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Black.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Black.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-BlackItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-BlackItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Bold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Bold.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-BoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-BoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ExtraBold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ExtraBold.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ExtraBoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ExtraBoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ExtraLight.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ExtraLight.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ExtraLightItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ExtraLightItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Light.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Light.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-LightItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-LightItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Medium.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Medium.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-MediumItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-MediumItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Regular.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-SemiBold.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-SemiBold.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-SemiBoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-SemiBoldItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Thin.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-Thin.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ThinItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterDisplay-ThinItalic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterVariable-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterVariable-Italic.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/font/inter/InterVariable.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/font/inter/InterVariable.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										92
									
								
								public/font/inter/LICENSE.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								public/font/inter/LICENSE.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | ||||||
|  | Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) | ||||||
|  | 
 | ||||||
|  | This Font Software is licensed under the SIL Open Font License, Version 1.1. | ||||||
|  | This license is copied below, and is also available with a FAQ at: | ||||||
|  | http://scripts.sil.org/OFL | ||||||
|  | 
 | ||||||
|  | ----------------------------------------------------------- | ||||||
|  | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | ||||||
|  | ----------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | PREAMBLE | ||||||
|  | The goals of the Open Font License (OFL) are to stimulate worldwide | ||||||
|  | development of collaborative font projects, to support the font creation | ||||||
|  | efforts of academic and linguistic communities, and to provide a free and | ||||||
|  | open framework in which fonts may be shared and improved in partnership | ||||||
|  | with others. | ||||||
|  | 
 | ||||||
|  | The OFL allows the licensed fonts to be used, studied, modified and | ||||||
|  | redistributed freely as long as they are not sold by themselves. The | ||||||
|  | fonts, including any derivative works, can be bundled, embedded, | ||||||
|  | redistributed and/or sold with any software provided that any reserved | ||||||
|  | names are not used by derivative works. The fonts and derivatives, | ||||||
|  | however, cannot be released under any other type of license. The | ||||||
|  | requirement for fonts to remain under this license does not apply | ||||||
|  | to any document created using the fonts or their derivatives. | ||||||
|  | 
 | ||||||
|  | DEFINITIONS | ||||||
|  | "Font Software" refers to the set of files released by the Copyright | ||||||
|  | Holder(s) under this license and clearly marked as such. This may | ||||||
|  | include source files, build scripts and documentation. | ||||||
|  | 
 | ||||||
|  | "Reserved Font Name" refers to any names specified as such after the | ||||||
|  | copyright statement(s). | ||||||
|  | 
 | ||||||
|  | "Original Version" refers to the collection of Font Software components as | ||||||
|  | distributed by the Copyright Holder(s). | ||||||
|  | 
 | ||||||
|  | "Modified Version" refers to any derivative made by adding to, deleting, | ||||||
|  | or substituting -- in part or in whole -- any of the components of the | ||||||
|  | Original Version, by changing formats or by porting the Font Software to a | ||||||
|  | new environment. | ||||||
|  | 
 | ||||||
|  | "Author" refers to any designer, engineer, programmer, technical | ||||||
|  | writer or other person who contributed to the Font Software. | ||||||
|  | 
 | ||||||
|  | PERMISSION AND CONDITIONS | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining | ||||||
|  | a copy of the Font Software, to use, study, copy, merge, embed, modify, | ||||||
|  | redistribute, and sell modified and unmodified copies of the Font | ||||||
|  | Software, subject to the following conditions: | ||||||
|  | 
 | ||||||
|  | 1) Neither the Font Software nor any of its individual components, | ||||||
|  | in Original or Modified Versions, may be sold by itself. | ||||||
|  | 
 | ||||||
|  | 2) Original or Modified Versions of the Font Software may be bundled, | ||||||
|  | redistributed and/or sold with any software, provided that each copy | ||||||
|  | contains the above copyright notice and this license. These can be | ||||||
|  | included either as stand-alone text files, human-readable headers or | ||||||
|  | in the appropriate machine-readable metadata fields within text or | ||||||
|  | binary files as long as those fields can be easily viewed by the user. | ||||||
|  | 
 | ||||||
|  | 3) No Modified Version of the Font Software may use the Reserved Font | ||||||
|  | Name(s) unless explicit written permission is granted by the corresponding | ||||||
|  | Copyright Holder. This restriction only applies to the primary font name as | ||||||
|  | presented to the users. | ||||||
|  | 
 | ||||||
|  | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | ||||||
|  | Software shall not be used to promote, endorse or advertise any | ||||||
|  | Modified Version, except to acknowledge the contribution(s) of the | ||||||
|  | Copyright Holder(s) and the Author(s) or with their explicit written | ||||||
|  | permission. | ||||||
|  | 
 | ||||||
|  | 5) The Font Software, modified or unmodified, in part or in whole, | ||||||
|  | must be distributed entirely under this license, and must not be | ||||||
|  | distributed under any other license. The requirement for fonts to | ||||||
|  | remain under this license does not apply to any document created | ||||||
|  | using the Font Software. | ||||||
|  | 
 | ||||||
|  | TERMINATION | ||||||
|  | This license becomes null and void if any of the above conditions are | ||||||
|  | not met. | ||||||
|  | 
 | ||||||
|  | DISCLAIMER | ||||||
|  | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||||
|  | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | ||||||
|  | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | ||||||
|  | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | ||||||
|  | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | ||||||
|  | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | ||||||
|  | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||||||
|  | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | ||||||
|  | OTHER DEALINGS IN THE FONT SOFTWARE. | ||||||
							
								
								
									
										57
									
								
								public/font/inter/inter.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								public/font/inter/inter.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | ||||||
|  | /* Variable fonts usage: | ||||||
|  | :root { font-family: "Inter", sans-serif; } | ||||||
|  | @supports (font-variation-settings: normal) { | ||||||
|  |   :root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; } | ||||||
|  | } */ | ||||||
|  | @font-face { | ||||||
|  |   font-family: InterVariable; | ||||||
|  |   font-style: normal; | ||||||
|  |   font-weight: 100 900; | ||||||
|  |   font-display: swap; | ||||||
|  |   src: url("InterVariable.woff2") format("woff2"); | ||||||
|  | } | ||||||
|  | @font-face { | ||||||
|  |   font-family: InterVariable; | ||||||
|  |   font-style: italic; | ||||||
|  |   font-weight: 100 900; | ||||||
|  |   font-display: swap; | ||||||
|  |   src: url("InterVariable-Italic.woff2") format("woff2"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* static fonts */ | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 100; font-display: swap; src: url("Inter-Thin.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 100; font-display: swap; src: url("Inter-ThinItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 200; font-display: swap; src: url("Inter-ExtraLight.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 200; font-display: swap; src: url("Inter-ExtraLightItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 300; font-display: swap; src: url("Inter-Light.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 300; font-display: swap; src: url("Inter-LightItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("Inter-Regular.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 400; font-display: swap; src: url("Inter-Italic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("Inter-Medium.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 500; font-display: swap; src: url("Inter-MediumItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("Inter-SemiBold.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 600; font-display: swap; src: url("Inter-SemiBoldItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("Inter-Bold.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 700; font-display: swap; src: url("Inter-BoldItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 800; font-display: swap; src: url("Inter-ExtraBold.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 800; font-display: swap; src: url("Inter-ExtraBoldItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: normal; font-weight: 900; font-display: swap; src: url("Inter-Black.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "Inter"; font-style: italic; font-weight: 900; font-display: swap; src: url("Inter-BlackItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 100; font-display: swap; src: url("InterDisplay-Thin.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 100; font-display: swap; src: url("InterDisplay-ThinItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLight.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLightItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 300; font-display: swap; src: url("InterDisplay-Light.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 300; font-display: swap; src: url("InterDisplay-LightItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 400; font-display: swap; src: url("InterDisplay-Regular.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 400; font-display: swap; src: url("InterDisplay-Italic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 500; font-display: swap; src: url("InterDisplay-Medium.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 500; font-display: swap; src: url("InterDisplay-MediumItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBold.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBoldItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 700; font-display: swap; src: url("InterDisplay-Bold.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 700; font-display: swap; src: url("InterDisplay-BoldItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBold.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBoldItalic.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 900; font-display: swap; src: url("InterDisplay-Black.woff2") format("woff2"); } | ||||||
|  | @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 900; font-display: swap; src: url("InterDisplay-BlackItalic.woff2") format("woff2"); } | ||||||
|  | @ -1,107 +0,0 @@ | ||||||
| <!DOCTYPE html> |  | ||||||
| <!-- |  | ||||||
|     experimenting with post layouts here! |  | ||||||
|     don't expect anything too flashy ;3 |  | ||||||
| 
 |  | ||||||
|     ari melody, 2024 |  | ||||||
| --> |  | ||||||
| <html lang="en"> |  | ||||||
|     <head> |  | ||||||
|         <meta charset="UTF-8"> |  | ||||||
|         <meta name="viewport" content="width=device-width, initial-scale=1"> |  | ||||||
|         <title></title> |  | ||||||
|         <link href="css/style.css" rel="stylesheet"> |  | ||||||
|         <script type="application/javascript" src="script/main.mjs" defer></script> |  | ||||||
|     </head> |  | ||||||
|     <body> |  | ||||||
|         <audio id="sound-success" src="sound/success.wav"></audio> |  | ||||||
|         <header> |  | ||||||
|              |  | ||||||
|         </header> |  | ||||||
|         <main> |  | ||||||
|             <div id="feed"> |  | ||||||
|                 <!-- |  | ||||||
|                 <div class="post-container" aria-label="ari; hello world!~; 02:12:06"> |  | ||||||
|                     <div class="post-context"> |  | ||||||
|                         <span class="post-context-icon">🔁</span> |  | ||||||
|                         <span class="post-context-action"> |  | ||||||
|                             <a href="/@ari">ari 💫</a> boosted this post. |  | ||||||
|                         </span> |  | ||||||
|                         <span class="post-context-time"> |  | ||||||
|                             <time title="6/3/2024, 2:12:06 AM">2m ago</time> |  | ||||||
|                         </span> |  | ||||||
|                     </div> |  | ||||||
|                     <article class="post"> |  | ||||||
|                         <div class="post-header-container"> |  | ||||||
|                             <a href="/@ari" class="post-avatar-container"> |  | ||||||
|                                 <img src="avatar/ari.jpg" alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> |  | ||||||
|                             </a> |  | ||||||
|                             <header class="post-header"> |  | ||||||
|                                 <div class="post-user-info"> |  | ||||||
|                                     <a href="/@ari" class="name">ari 💫</a> |  | ||||||
|                                     <span class="username">@ari</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="post-info"> |  | ||||||
|                                     <a href="/post/21c892b23701" class="created-at"> |  | ||||||
|                                         <time title="6/3/2024, 2:11:58 AM">10m ago</time> |  | ||||||
|                                     </a> |  | ||||||
|                                 </div> |  | ||||||
|                             </header> |  | ||||||
|                         </div> |  | ||||||
|                         <div class="post-body"> |  | ||||||
|                             <span class="post-content">hello world!~</span> |  | ||||||
|                             <div class="post-media-container" data-count="3"> |  | ||||||
|                                 <div class="post-media image"> |  | ||||||
|                                     <a href="media/ariyeah-button.png"> |  | ||||||
|                                         <img src="media/ariyeah-button.png" alt="custom miiverse "yeah!" button" loading="lazy" decoding="async"> |  | ||||||
|                                     </a> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="post-media image"> |  | ||||||
|                                     <a href="media/beer.jpg"> |  | ||||||
|                                         <img src="media/beer.jpg" alt="barney calhoun with beer" loading="lazy" decoding="async"> |  | ||||||
|                                     </a> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="post-media image"> |  | ||||||
|                                     <a href="media/duck.jpg"> |  | ||||||
|                                         <img src="media/duck.jpg" alt="big rubber duck" loading="lazy" decoding="async"> |  | ||||||
|                                     </a> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                         <footer class="post-footer"> |  | ||||||
|                             <div class="post-reactions"> |  | ||||||
|                                 <button type="button" class="reaction"> |  | ||||||
|                                     <span>⭐</span> |  | ||||||
|                                     <span class="count">52</span> |  | ||||||
|                                 </button> |  | ||||||
|                             </div> |  | ||||||
|                             <div class="post-actions"> |  | ||||||
|                                 <button type="button" class="reply" aria-label="Reply" title="Reply"> |  | ||||||
|                                     <span>🗨️</span> |  | ||||||
|                                     <span class="count">7</span> |  | ||||||
|                                 </button> |  | ||||||
|                                 <button type="button" class="boost" aria-label="Boost" title="Boost"> |  | ||||||
|                                     <span>🔁</span> |  | ||||||
|                                     <span class="count">13</span> |  | ||||||
|                                 </button> |  | ||||||
|                                 <button type="button" class="favourite" aria-label="Favourite" title="Favourite"> |  | ||||||
|                                     <span>⭐</span> |  | ||||||
|                                 </button> |  | ||||||
|                                 <button type="button" class="react" aria-label="React" title="React"> |  | ||||||
|                                     <span>😃</span> |  | ||||||
|                                 </button> |  | ||||||
|                                 <button type="button" class="quote" aria-label="Quote" title="Quote"> |  | ||||||
|                                     <span>🗣️</span> |  | ||||||
|                                 </button> |  | ||||||
|                                 <button type="button" class="more" aria-label="More" title="More"> |  | ||||||
|                                     <span>🛠️</span> |  | ||||||
|                                 </button> |  | ||||||
|                             </div> |  | ||||||
|                         </footer> |  | ||||||
|                     </article> |  | ||||||
|                 </div> |  | ||||||
|                 --> |  | ||||||
|             </div> |  | ||||||
|         </main> |  | ||||||
|     </body> |  | ||||||
| </html> |  | ||||||
|  | @ -1,302 +0,0 @@ | ||||||
| const aria_safe_regex = /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF]|[\r])/g; |  | ||||||
| const INSTANCE_URL = "soc.arimelody.me"; |  | ||||||
| 
 |  | ||||||
| const sounds = { |  | ||||||
|     "default": new Audio("sound/log.ogg"), |  | ||||||
|     "post": new Audio("sound/success.ogg"), |  | ||||||
|     "boost": new Audio("sound/hello.ogg"), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const actors = { |  | ||||||
|     "@ari": { |  | ||||||
|         "url": "https://soc.arimelody.me/@ari", |  | ||||||
|         "name": "ari 💫", |  | ||||||
|         "avatar": "avatar/ari.jpg", |  | ||||||
|     } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const test_post = { |  | ||||||
|     "context": { |  | ||||||
|         "type": "boost", |  | ||||||
|         "by": "@ari", |  | ||||||
|         "at": 1718513838624, |  | ||||||
|     }, |  | ||||||
|     "author": "@ari", |  | ||||||
|     "url": "/post/21c892b23701", |  | ||||||
|     "at": 1718513988384, |  | ||||||
|     "content": "hello world!~", |  | ||||||
|     "media": [ |  | ||||||
|         { "url": "media/ariyeah-button.png", "alt": "custom miiverse \"yeah!\" button" }, |  | ||||||
|         { "url": "media/beer.jpg", "alt": "barney calhoun with beer" }, |  | ||||||
|         { "url": "media/duck.jpg", "alt": "big rubber duck" }, |  | ||||||
|     ], |  | ||||||
|     "replies": 7, |  | ||||||
|     "boosts": 13, |  | ||||||
|     "reactions": [ |  | ||||||
|         { "react": "⭐", "count": "52" }, |  | ||||||
|         { "react": "❤️", "count": "9" }, |  | ||||||
|     ], |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const feed = document.getElementById("feed"); |  | ||||||
| 
 |  | ||||||
| function render_post(data) { |  | ||||||
|     // TODO: please god just use or make a library to build this
 |  | ||||||
| 
 |  | ||||||
|     const actor = actors[data.author]; |  | ||||||
|     if (!actor) return; |  | ||||||
|     const date = new Date(data.at); |  | ||||||
| 
 |  | ||||||
|     const post = document.createElement("article"); |  | ||||||
|     post.classList.add("post-container"); |  | ||||||
|     post.ariaLabel = actor.name.replace(aria_safe_regex, "").trim() + "; " + data.content + "; " + date.toLocaleTimeString(); |  | ||||||
| 
 |  | ||||||
|     if (data.context && data.context.by && actors[data.context.by]) { |  | ||||||
|         // post context
 |  | ||||||
|         const post_context = document.createElement("div"); |  | ||||||
|         post_context.classList.add("post-context"); |  | ||||||
| 
 |  | ||||||
|         if (data.context.type == "boost") { |  | ||||||
|             const post_context_icon = document.createElement("span"); |  | ||||||
|             post_context_icon.classList.add("post-context-icon"); |  | ||||||
|             post_context_icon.innerText = "🔁"; |  | ||||||
|             post_context.appendChild(post_context_icon); |  | ||||||
| 
 |  | ||||||
|             const post_context_action = document.createElement("span"); |  | ||||||
|             post_context_action.classList.add("post-context-action"); |  | ||||||
|             const actor = actors[data.context.by]; |  | ||||||
|             post_context_action.innerHTML = `boosted by <a href="${actor.url}">${actor.name}</a>`; |  | ||||||
|             post_context.appendChild(post_context_action); |  | ||||||
| 
 |  | ||||||
|             const post_context_time = document.createElement("span"); |  | ||||||
|             post_context_time.classList.add("post-context-time"); |  | ||||||
|             post_context_time.innerHTML = `<time>${new Date(data.context.at).toLocaleString()}</time>`; |  | ||||||
|             post_context.appendChild(post_context_time); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         post.appendChild(post_context); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // the actual post
 |  | ||||||
|     // article.post
 |  | ||||||
|     const post_article = document.createElement("article"); |  | ||||||
| 
 |  | ||||||
|     const post_header_container = document.createElement("div"); |  | ||||||
|     post_header_container.classList.add("post-header-container"); |  | ||||||
|     const post_avatar_container = document.createElement("a"); |  | ||||||
|     post_avatar_container.classList.add("post-avatar-container"); |  | ||||||
|     post_avatar_container.href = actor.url; |  | ||||||
|     const post_avatar = document.createElement("img"); |  | ||||||
|     post_avatar.classList.add("post-avatar"); |  | ||||||
|     post_avatar.src = actor.avatar; |  | ||||||
|     post_avatar.alt = ""; |  | ||||||
|     post_avatar.width = 48; |  | ||||||
|     post_avatar.height = 48; |  | ||||||
|     post_avatar.loading = "lazy"; |  | ||||||
|     post_avatar.decoding = "async"; |  | ||||||
|     post_avatar_container.appendChild(post_avatar); |  | ||||||
|     post_header_container.appendChild(post_avatar_container); |  | ||||||
| 
 |  | ||||||
|     const post_header = document.createElement("header"); |  | ||||||
|     post_header.classList.add("post-header"); |  | ||||||
|     const post_user_info = document.createElement("div"); |  | ||||||
|     post_user_info.classList.add("post-user-info"); |  | ||||||
|     const post_user_info_name = document.createElement("a"); |  | ||||||
|     post_user_info_name.classList.add("name"); |  | ||||||
|     post_user_info_name.href = actor.url; |  | ||||||
|     post_user_info_name.innerText = actor.name |  | ||||||
|     post_user_info.appendChild(post_user_info_name); |  | ||||||
|     const post_user_info_username = document.createElement("span"); |  | ||||||
|     post_user_info_username.classList.add("username"); |  | ||||||
|     post_user_info_username.href = actor.url; |  | ||||||
|     post_user_info_username.innerText = data.author |  | ||||||
|     post_user_info.appendChild(post_user_info_username); |  | ||||||
|     post_header.appendChild(post_user_info); |  | ||||||
|     const post_info = document.createElement("div"); |  | ||||||
|     post_info.classList.add("post-info"); |  | ||||||
|     const post_info_time = document.createElement("a"); |  | ||||||
|     post_info_time.classList.add("created-at"); |  | ||||||
|     const post_date = new Date(data.at); |  | ||||||
|     post_info_time.innerHTML = `<time title=${post_date.toLocaleString()}>${post_date.toLocaleString()}</time>`; |  | ||||||
|     post_info_time.href = post.url; |  | ||||||
|     post_info.appendChild(post_info_time); |  | ||||||
|     post_header.appendChild(post_info); |  | ||||||
|     post_header_container.appendChild(post_header); |  | ||||||
| 
 |  | ||||||
|     post_article.appendChild(post_header_container); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     const post_body = document.createElement("div"); |  | ||||||
|     post_body.classList.add("post-body"); |  | ||||||
| 
 |  | ||||||
|     const post_content = document.createElement("span"); |  | ||||||
|     post_content.classList.add("post-content"); |  | ||||||
|     post_content.innerText = data.content; |  | ||||||
|     post_body.appendChild(post_content); |  | ||||||
| 
 |  | ||||||
|     const media_container = document.createElement("div"); |  | ||||||
|     media_container.classList.add("post-media-container"); |  | ||||||
|     media_container.dataset.count = data.media.length; |  | ||||||
|     data.media.forEach(media => { |  | ||||||
|         const media_item = document.createElement("div"); |  | ||||||
|         media_item.classList.add("post-media"); |  | ||||||
|         const link = document.createElement("a"); |  | ||||||
|         link.href = media.url; |  | ||||||
|         const source = document.createElement("img"); |  | ||||||
|         source.src = media.url; |  | ||||||
|         source.alt = media.alt; |  | ||||||
|         source.loading = "lazy"; |  | ||||||
|         source.decoding = "async"; |  | ||||||
|         link.appendChild(source); |  | ||||||
|         media_item.appendChild(link); |  | ||||||
|         media_container.appendChild(media_item); |  | ||||||
|     }); |  | ||||||
|     post_body.appendChild(media_container); |  | ||||||
| 
 |  | ||||||
|     post_article.appendChild(post_body); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     const post_footer = document.createElement("footer"); |  | ||||||
|     post_footer.classList.add("post-footer"); |  | ||||||
| 
 |  | ||||||
|     const post_reactions = document.createElement("div"); |  | ||||||
|     post_reactions.classList.add("post-reactions"); |  | ||||||
|     data.reactions.forEach(reaction => { |  | ||||||
|         const btn = document.createElement("button"); |  | ||||||
|         btn.classList.add("reaction"); |  | ||||||
|         btn.type = "button"; |  | ||||||
|         const emote = document.createElement("span"); |  | ||||||
|         emote.innerText = reaction.react; |  | ||||||
|         btn.appendChild(emote); |  | ||||||
|         const count = document.createElement("span"); |  | ||||||
|         count.classList.add("count"); |  | ||||||
|         count.innerText = reaction.count; |  | ||||||
|         btn.appendChild(count); |  | ||||||
|         post_reactions.appendChild(btn); |  | ||||||
|     }); |  | ||||||
|     post_footer.appendChild(post_reactions); |  | ||||||
| 
 |  | ||||||
|     const post_actions = document.createElement("div"); |  | ||||||
|     post_actions.classList.add("post-actions"); |  | ||||||
| 
 |  | ||||||
|     const reply_button = document.createElement("button"); |  | ||||||
|     reply_button.type = "button"; |  | ||||||
|     reply_button.ariaLabel = "Reply"; |  | ||||||
|     reply_button.title = "Reply"; |  | ||||||
|     reply_button.innerHTML = `<span>🗨️</span><span class="count">${data.replies}</count>`; |  | ||||||
|     post_actions.appendChild(reply_button); |  | ||||||
| 
 |  | ||||||
|     const boost_button = document.createElement("button"); |  | ||||||
|     boost_button.type = "button"; |  | ||||||
|     boost_button.ariaLabel = "Boost"; |  | ||||||
|     boost_button.title = "Boost"; |  | ||||||
|     boost_button.innerHTML = `<span>🔁</span><span class="count">${data.boosts}</count>`; |  | ||||||
|     post_actions.appendChild(boost_button); |  | ||||||
| 
 |  | ||||||
|     const fav_button = document.createElement("button"); |  | ||||||
|     fav_button.type = "button"; |  | ||||||
|     fav_button.ariaLabel = "Favourite"; |  | ||||||
|     fav_button.title = "Favourite"; |  | ||||||
|     fav_button.innerText = "⭐"; |  | ||||||
|     post_actions.appendChild(fav_button); |  | ||||||
| 
 |  | ||||||
|     const react_button = document.createElement("button"); |  | ||||||
|     react_button.type = "button"; |  | ||||||
|     react_button.ariaLabel = "React"; |  | ||||||
|     react_button.title = "React"; |  | ||||||
|     react_button.innerText = "😃"; |  | ||||||
|     post_actions.appendChild(react_button); |  | ||||||
| 
 |  | ||||||
|     const quote_button = document.createElement("button"); |  | ||||||
|     quote_button.type = "button"; |  | ||||||
|     quote_button.ariaLabel = "Quote"; |  | ||||||
|     quote_button.title = "Quote"; |  | ||||||
|     quote_button.innerText = "🗣️"; |  | ||||||
|     post_actions.appendChild(quote_button); |  | ||||||
| 
 |  | ||||||
|     const more_button = document.createElement("button"); |  | ||||||
|     more_button.type = "button"; |  | ||||||
|     more_button.ariaLabel = "More"; |  | ||||||
|     more_button.title = "More"; |  | ||||||
|     more_button.innerText = "⚒️"; |  | ||||||
|     post_actions.appendChild(more_button); |  | ||||||
| 
 |  | ||||||
|     post_footer.appendChild(post_actions); |  | ||||||
| 
 |  | ||||||
|     post_article.appendChild(post_footer); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     post.appendChild(post_article); |  | ||||||
| 
 |  | ||||||
|     return post; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function hook_post_listeners(post) { |  | ||||||
|     post.querySelectorAll("button").forEach(button => { |  | ||||||
|         button.addEventListener("click", () => { |  | ||||||
|             if (button.classList.contains("reaction")) { |  | ||||||
|                 toggle_reaction(button); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             switch (button.ariaLabel) { |  | ||||||
|                 case "Reply": |  | ||||||
|                     play_sound("post"); |  | ||||||
|                     break; |  | ||||||
| 
 |  | ||||||
|                 case "Boost": |  | ||||||
|                     play_sound("boost"); |  | ||||||
|                     break; |  | ||||||
| 
 |  | ||||||
|                 case "Favourite": |  | ||||||
|                     post.querySelectorAll("button.reaction").forEach(reaction => { |  | ||||||
|                         if (!reaction.innerText.startsWith("⭐")) return; |  | ||||||
|                         toggle_reaction(reaction); |  | ||||||
|                     }); |  | ||||||
|                     play_sound(); |  | ||||||
|                     break; |  | ||||||
| 
 |  | ||||||
|                 default: |  | ||||||
|                     play_sound(); |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         });        |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function toggle_reaction(reaction) { |  | ||||||
|     const was_active = reaction.classList.contains("active"); |  | ||||||
|     reaction.classList.toggle("active"); |  | ||||||
|     const count = reaction.querySelector(".count"); |  | ||||||
|     count.innerText = Number(count.innerText) + (was_active ? -1 : 1); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function load_content() { |  | ||||||
|     for (let i = 0; i < 10; i++) { |  | ||||||
|         const post = render_post(test_post); |  | ||||||
|         feed.appendChild(post); |  | ||||||
|         hook_post_listeners(post); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function play_sound(name) { |  | ||||||
|     if (!name) name = "default"; |  | ||||||
|     const sound = sounds[name]; |  | ||||||
|     if (!sound) { |  | ||||||
|         console.warn(`Attempted to play sound "${name}", which does not exist!`); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|     sound.pause(); |  | ||||||
|     sound.currentTime = 0; |  | ||||||
|     sound.play(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| feed.querySelectorAll(".post-container").forEach(post => { |  | ||||||
|     hook_post_listeners(post); |  | ||||||
| }); |  | ||||||
| load_content(); |  | ||||||
| 
 |  | ||||||
| document.addEventListener("scroll", event => { |  | ||||||
|     while (window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000) { |  | ||||||
|         load_content(); |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
							
								
								
									
										154
									
								
								src/App.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/App.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,154 @@ | ||||||
|  | <script> | ||||||
|  |     import Feed from './Feed.svelte'; | ||||||
|  |     import Error from './Error.svelte'; | ||||||
|  |     import Instance from './instance.js'; | ||||||
|  | 
 | ||||||
|  |     let ready = false; | ||||||
|  |     if (localStorage.getItem("fedi_host") && localStorage.getItem("fedi_token")) { | ||||||
|  |         Instance.setup( | ||||||
|  |             localStorage.getItem("fedi_host"), | ||||||
|  |             localStorage.getItem("fedi_token"), | ||||||
|  |             true | ||||||
|  |         ).then(() => { | ||||||
|  |             ready = true; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function log_in(event) { | ||||||
|  |         event.preventDefault(); | ||||||
|  |         localStorage.setItem("fedi_host", event.target.instance_host.value); | ||||||
|  |         localStorage.setItem("fedi_token", event.target.session_token.value); | ||||||
|  |         location = location; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function log_out() { | ||||||
|  |         localStorage.removeItem("fedi_host"); | ||||||
|  |         localStorage.removeItem("fedi_token"); | ||||||
|  |         location = location; | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <header> | ||||||
|  |     <h1>space social</h1> | ||||||
|  |     <p>social media for the galaxy-wide-web! 🌌</p> | ||||||
|  |     <button id="log-out" on:click={log_out}>log out</button> | ||||||
|  | </header> | ||||||
|  | 
 | ||||||
|  | <main> | ||||||
|  |     {#if ready} | ||||||
|  |         <Feed /> | ||||||
|  |     {:else if !Instance.get_instance().ok} | ||||||
|  |         <Error> | ||||||
|  |             <p>this app requires a <strong>instance host</strong> and <strong>session token</strong> to work! you may enter these below:</p> | ||||||
|  | 
 | ||||||
|  |             <form on:submit={data => (log_in(data))}> | ||||||
|  |                 <label for="instance host">instance host: </label> | ||||||
|  |                 <input type="text" id="instance_host"> | ||||||
|  |                 <br> | ||||||
|  |                 <label for="session token">session token: </label> | ||||||
|  |                 <input type="password" id="session_token"> | ||||||
|  |                 <br> | ||||||
|  |                 <button type="submit" id="log-in">log in</button> | ||||||
|  |             </form> | ||||||
|  | 
 | ||||||
|  |             <hr> | ||||||
|  | 
 | ||||||
|  |             <h4>how do i get these?</h4> | ||||||
|  |             <ul> | ||||||
|  |                 <li> | ||||||
|  |                     <strong>instance host</strong> refers to the domain of your fediverse instance. i.e. <code>ice.arimelody.me</code>. | ||||||
|  |                 </li> | ||||||
|  |                 <li> | ||||||
|  |                     a <strong>token</strong> is a unique code that grants applications permission to act on your behalf. | ||||||
|  |                     you can find it in your browser's cookies for your instance. | ||||||
|  |                     (instructions for <a href="https://support.mozilla.org/en-US/questions/1219653">firefox</a> | ||||||
|  |                     and <a href="https://superuser.com/questions/1715037/how-can-i-view-the-content-of-cookies-in-chrome">chrome</a>) | ||||||
|  |                 </li> | ||||||
|  |             </ul> | ||||||
|  | 
 | ||||||
|  |             <p><small> | ||||||
|  |                 your login credentials will not be saved to an external server. | ||||||
|  |                 they are required for communication with the fediverse instance, and are saved entirely within your browser. | ||||||
|  |                 a cleaner login flow will be built in the future. | ||||||
|  |             </small></p> | ||||||
|  |             <p><small> | ||||||
|  |                 oh yeah i should also probably mention this is <strong><em>extremely experimental software</em></strong>; | ||||||
|  |                 even if you use the exact same instance as me, you may encounter problems. | ||||||
|  |                 if that's all cool with you, welcome aboard! | ||||||
|  |             </small></p> | ||||||
|  | 
 | ||||||
|  |             <p>made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p> | ||||||
|  |         </Error> | ||||||
|  |     {/if} | ||||||
|  | </main> | ||||||
|  | 
 | ||||||
|  | <footer> | ||||||
|  | </footer> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     header { | ||||||
|  |         width: min(768px, calc(100vw - 32px)); | ||||||
|  |         margin: 16px auto; | ||||||
|  |         padding: 0 16px; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |         align-items: center; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     h1 { | ||||||
|  |         color: var(--accent); | ||||||
|  |         margin: 0 16px 0 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     main { | ||||||
|  |         width: min(800px, calc(100vw - 16px)); | ||||||
|  |         margin: 0 auto; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     a { | ||||||
|  |         color: var(--accent); | ||||||
|  |         text-decoration: none; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     a:hover { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     input[type="text"], input[type="password"] { | ||||||
|  |         margin-bottom: 8px; | ||||||
|  |         padding: 4px 6px; | ||||||
|  |         font-family: inherit; | ||||||
|  |         border: none; | ||||||
|  |         border-radius: 8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button#log-in, button#log-out { | ||||||
|  |         margin-left: auto; | ||||||
|  |         padding: 8px 12px; | ||||||
|  |         font-size: 1em; | ||||||
|  |         background-color: var(--bg2); | ||||||
|  |         color: inherit; | ||||||
|  |         border: none; | ||||||
|  |         border-radius: 16px; | ||||||
|  |         cursor: pointer; | ||||||
|  |         transition: color .1s, background-color .1s; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button#log-in.active, button#log-out.active { | ||||||
|  |         background: var(--accent); | ||||||
|  |         color: var(--bg0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button#log-in:hover, button#log-out:hover { | ||||||
|  |         color: var(--bg0); | ||||||
|  |         background: var(--fg0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button#log-in:active, button#log-out:active { | ||||||
|  |         background: #0001; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     code { | ||||||
|  |         font-size: 1.2em; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										26
									
								
								src/Error.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/Error.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | <script> | ||||||
|  |     export let msg = ""; | ||||||
|  |     export let trace = ""; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="error"> | ||||||
|  |     {#if msg} | ||||||
|  |         <p class="msg">{@html msg}</p> | ||||||
|  |     {/if} | ||||||
|  |     {#if trace} | ||||||
|  |         <pre class="trace">{trace}</pre> | ||||||
|  |     {/if} | ||||||
|  |     <slot></slot> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .error { | ||||||
|  |         margin-top: 16px; | ||||||
|  |         padding: 20px 32px; | ||||||
|  |         border: 1px solid #8884; | ||||||
|  |         border-radius: 16px; | ||||||
|  |         background-color: var(--bg1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										49
									
								
								src/Feed.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/Feed.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | ||||||
|  | <script> | ||||||
|  |     import Post from './post/Post.svelte'; | ||||||
|  |     import Error from './Error.svelte'; | ||||||
|  |     import Instance from './instance.js'; | ||||||
|  | 
 | ||||||
|  |     let posts = []; | ||||||
|  |     let loading = false; | ||||||
|  | 
 | ||||||
|  |     let error; | ||||||
|  | 
 | ||||||
|  |     async function load_posts() { | ||||||
|  |         if (loading) return; // no spamming!! | ||||||
|  |         loading = true; | ||||||
|  | 
 | ||||||
|  |         let new_posts = []; | ||||||
|  |         if (posts.length === 0) new_posts = await Instance.get_timeline() | ||||||
|  |         else new_posts = await Instance.get_timeline(posts[posts.length - 1].id); | ||||||
|  |          | ||||||
|  |         if (!new_posts) { | ||||||
|  |             error = `sorry! the frontend is unable to communicate with your server. | ||||||
|  | 
 | ||||||
|  | this app is still in very early development, and is currently only built to support iceshrimp. | ||||||
|  | 
 | ||||||
|  | for more information, please consult the developer console.`; | ||||||
|  |             loading = false; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         posts = [...posts, ...new_posts]; | ||||||
|  | 
 | ||||||
|  |         loading = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     load_posts(); | ||||||
|  |     document.addEventListener("scroll", event => { | ||||||
|  |         if (!loading && window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||||
|  |             load_posts(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div id="feed"> | ||||||
|  |     {#if error} | ||||||
|  |         <Error msg={error.replaceAll('\n', '<br>')} /> | ||||||
|  |     {/if} | ||||||
|  |     {#each posts as post} | ||||||
|  |         <Post post={post} /> | ||||||
|  |     {/each} | ||||||
|  | </div> | ||||||
							
								
								
									
										20
									
								
								src/app.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/app.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | @import url("/font/inter/inter.css"); | ||||||
|  | 
 | ||||||
|  | :root { | ||||||
|  |     --fg0: #eee; | ||||||
|  |     --bg0: #080808; | ||||||
|  |     --bg1: #101010; | ||||||
|  |     --bg2: #121212; | ||||||
|  |     --accent: #b7fd49; | ||||||
|  |     --accent-bg: #242b1a; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | body { | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  | 
 | ||||||
|  |     color: var(--fg0); | ||||||
|  |     background-color: var(--bg0); | ||||||
|  | 
 | ||||||
|  |     font-family: "Inter", sans-serif; | ||||||
|  | } | ||||||
							
								
								
									
										93
									
								
								src/emoji.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/emoji.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | ||||||
|  | import Instance from './instance.js'; | ||||||
|  | 
 | ||||||
|  | const EMOJI_REGEX = /:[a-z0-9_\-]+:/g; | ||||||
|  | 
 | ||||||
|  | let emoji_cache = []; | ||||||
|  | 
 | ||||||
|  | export default class Emoji { | ||||||
|  |     name; | ||||||
|  |     host; | ||||||
|  |     url; | ||||||
|  |     width; | ||||||
|  |     height; | ||||||
|  | 
 | ||||||
|  |     static parse(data, host) { | ||||||
|  |         const instance = Instance.get_instance(); | ||||||
|  |         let emoji = null; | ||||||
|  |         switch (instance.type) { | ||||||
|  |             case Instance.types.ICESHRIMP: | ||||||
|  |                 emoji = Emoji.#parse_iceshrimp(data); | ||||||
|  |                 break; | ||||||
|  |             case Instance.types.MASTODON: | ||||||
|  |                 emoji = Emoji.#parse_mastodon(data); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         if (emoji !== null) emoji_cache.push(emoji); | ||||||
|  |         return emoji; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static #parse_iceshrimp(data, host) { | ||||||
|  |         let emoji = new Emoji() | ||||||
|  |         emoji.name = data.name.substring(1, data.name.search('@')); | ||||||
|  |         emoji.host = host; | ||||||
|  |         emoji.url = data.url; | ||||||
|  |         emoji.width = data.width; | ||||||
|  |         emoji.height = data.height; | ||||||
|  |         return emoji; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static #parse_mastodon(data, host) { | ||||||
|  |         let emoji = new Emoji() | ||||||
|  |         emoji.name = data.shortcode; | ||||||
|  |         emoji.host = host; | ||||||
|  |         emoji.url = data.url; | ||||||
|  |         emoji.width = data.width; | ||||||
|  |         emoji.height = data.height; | ||||||
|  |         return emoji; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get id() { | ||||||
|  |         return this.name + '@' + this.host; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function parse_text(text, ignore_instance) { | ||||||
|  |     if (!text) return text; | ||||||
|  |      | ||||||
|  |     let index = text.search(EMOJI_REGEX); | ||||||
|  |     if (index === -1) return text; | ||||||
|  |     index++; | ||||||
|  | 
 | ||||||
|  |     // find the emoji name
 | ||||||
|  |     let length = 0; | ||||||
|  |     while (index + length < text.length && text[index + length] !== ':') length++; | ||||||
|  |     let emoji_name = ':' + text.substring(index, index + length) + ':'; | ||||||
|  | 
 | ||||||
|  |     // does this emoji exist?
 | ||||||
|  |     let emoji; | ||||||
|  |     for (let cached in emoji_cache) { | ||||||
|  |         if (cached.id === emoji_name) { | ||||||
|  |             emoji = cached; | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!emoji) return text.substring(0, index + length) + parse_text(text.substring(index + length)); | ||||||
|  | 
 | ||||||
|  |     // replace emoji code with <img>
 | ||||||
|  |     const img = `<img src="${emoji.url}" class="emoji" width="26" height="26" title=":${emoji_name}:" alt="${emoji_name}">`; | ||||||
|  |     return text.substring(0, index - 1) + img + | ||||||
|  |         parse(text.substring(index + length + 1), emojis, ignore_instance); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function parse_one(reaction, emojis) { | ||||||
|  |     if (reaction == '❤') return '❤️'; // stupid heart unicode
 | ||||||
|  |     if (!reaction.startsWith(':') || !reaction.endsWith(':')) return reaction; | ||||||
|  |     for (let i = 0; i < emojis.length; i++) { | ||||||
|  |         if (emojis[i].name == reaction.substring(1, reaction.length - 1)) | ||||||
|  |             return `<img src="${emojis[i].url}" class="emoji" width="26" height="26" title="${reaction}" alt="${emojis[i].name}">`; | ||||||
|  |     } | ||||||
|  |     return reaction; | ||||||
|  | } | ||||||
							
								
								
									
										107
									
								
								src/instance.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/instance.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | ||||||
|  | import Post from './post/post.js'; | ||||||
|  | 
 | ||||||
|  | let instance; | ||||||
|  | 
 | ||||||
|  | const ERR_UNSUPPORTED = "Unsupported server"; | ||||||
|  | const ERR_SERVER_RESPONSE = "Unsupported response from the server"; | ||||||
|  | 
 | ||||||
|  | export default class Instance { | ||||||
|  |     #host; | ||||||
|  |     #token; | ||||||
|  |     #type; | ||||||
|  |     #secure; | ||||||
|  | 
 | ||||||
|  |     static types = { | ||||||
|  |         ICESHRIMP: "iceshrimp", | ||||||
|  |         MASTODON: "mastodon", | ||||||
|  |         MISSKEY: "misskey", | ||||||
|  |         AKKOMA: "akkoma", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     static get_instance() { | ||||||
|  |         if (!instance) instance = new Instance(); | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static async setup(host, token, secure) { | ||||||
|  |         instance = Instance.get_instance(); | ||||||
|  |         instance.host = host; | ||||||
|  |         instance.token = token; | ||||||
|  |         instance.secure = secure; | ||||||
|  |         await instance.#guess_type(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async #guess_type() { | ||||||
|  |         const url = instance.#proto + instance.host + "/api/v1/instance"; | ||||||
|  |         console.log("Snooping for instance information at " + url + "..."); | ||||||
|  |         const res = await fetch(url); | ||||||
|  |         const data = await res.json(); | ||||||
|  |         const version = data.version.toLowerCase(); | ||||||
|  | 
 | ||||||
|  |         instance.type = Instance.types.MASTODON; | ||||||
|  |         if (version.search("iceshrimp") !== -1) instance.type = Instance.types.ICESHRIMP; | ||||||
|  |         if (version.search("misskey") !== -1) instance.type = Instance.types.MISSKEY; | ||||||
|  |         if (version.search("akkoma") !== -1) instance.type = Instance.types.AKKOMA; | ||||||
|  | 
 | ||||||
|  |         console.log("Assumed server type to be \"" + instance.type + "\"."); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static async get_timeline(last_post_id) { | ||||||
|  |         let data = null; | ||||||
|  |         switch (instance.type) { | ||||||
|  |             case Instance.types.ICESHRIMP: | ||||||
|  |                 data = await instance.#get_timeline_iceshrimp(last_post_id); | ||||||
|  |                 break; | ||||||
|  |             case Instance.types.MASTODON: | ||||||
|  |                 data = await instance.#get_timeline_mastodon(last_post_id); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 console.error(ERR_UNSUPPORTED); | ||||||
|  |                 return null; | ||||||
|  |         } | ||||||
|  |         if (data.constructor != Array) { | ||||||
|  |             console.error(ERR_SERVER_RESPONSE); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         let posts = []; | ||||||
|  |         data.forEach(post_data => { | ||||||
|  |             const post = Post.parse(post_data); | ||||||
|  |             if (!post) return; | ||||||
|  |             posts = [...posts, post]; | ||||||
|  |         }); | ||||||
|  |         return posts; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async #get_timeline_iceshrimp(last_post_id) { | ||||||
|  |         let body = Object; | ||||||
|  |         if (last_post_id) body.untilId = last_post_id; | ||||||
|  |         const res = await fetch(this.#proto + this.host + "/api/notes/timeline", { | ||||||
|  |             method: 'POST', | ||||||
|  |             headers: { "Authorization": "Bearer " + this.token }, | ||||||
|  |             body: JSON.stringify(body) | ||||||
|  |         }); | ||||||
|  |         return await res.json(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async #get_timeline_mastodon(last_post_id) { | ||||||
|  |         let url = this.#proto + this.host + "/api/v1/timelines/home"; | ||||||
|  |         if (last_post_id) url += "?max_id=" + last_post_id; | ||||||
|  |         const res = await fetch(url, { | ||||||
|  |             method: 'GET', | ||||||
|  |             headers: { "Authorization": "Bearer " + this.token } | ||||||
|  |         }); | ||||||
|  |         return await res.json(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get #proto() { | ||||||
|  |         if (this.secure) return "https://"; | ||||||
|  |         return "http://"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static get ok() { | ||||||
|  |         if (!instance) return false; | ||||||
|  |         if (!instance.host) return false; | ||||||
|  |         if (!instance.token) return false; | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								src/main.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/main.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | import './app.css'; | ||||||
|  | import App from './App.svelte'; | ||||||
|  | import Instance from './instance.js'; | ||||||
|  | 
 | ||||||
|  | const app = new App({ | ||||||
|  |     target: document.getElementById('app') | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export default app; | ||||||
							
								
								
									
										166
									
								
								src/post/Body.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/post/Body.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,166 @@ | ||||||
|  | <script> | ||||||
|  |     export let post; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="post-body"> | ||||||
|  |     {#if post.warning} | ||||||
|  |         <p class="post-warning"><strong>{post.warning}</strong></p> | ||||||
|  |     {/if} | ||||||
|  |     {#if post.text} | ||||||
|  |         <span class="post-text">{@html post.rich_text}</span> | ||||||
|  |     {/if} | ||||||
|  |     <div class="post-media-container" data-count={post.files.length}> | ||||||
|  |         {#each post.files as file} | ||||||
|  |             <div class="post-media image"> | ||||||
|  |                 <a href={file.url}> | ||||||
|  |                     <img src={file.url} alt={file.alt} height="200" loading="lazy" decoding="async"> | ||||||
|  |                 </a> | ||||||
|  |             </div> | ||||||
|  |         {/each} | ||||||
|  |     </div> | ||||||
|  |     {#if post.boost && post.text} | ||||||
|  |         <p class="post-warning"><strong>this is quoting a post! quotes are not supported yet.</strong></p> | ||||||
|  |         <!-- TODO: quotes support --> | ||||||
|  |     {/if} | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .post-body { | ||||||
|  |         margin-top: 8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-warning { | ||||||
|  |         padding: 4px 8px; | ||||||
|  |         --warn-bg: rgba(255,220,30,.2); | ||||||
|  |         background-image: repeating-linear-gradient(-45deg, transparent, transparent 10px, var(--warn-bg) 10px, var(--warn-bg) 20px); | ||||||
|  |         border-radius: 8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text { | ||||||
|  |         word-wrap: break-word; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text :global(code) { | ||||||
|  |         font-size: 1.2em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text :global(code:has(pre)) { | ||||||
|  |         margin: 8px 0; | ||||||
|  |         padding: 8px; | ||||||
|  |         display: block; | ||||||
|  |         overflow-x: scroll; | ||||||
|  |         border-radius: 8px; | ||||||
|  |         background-color: #080808; | ||||||
|  |         color: var(--accent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text :global(code pre) { | ||||||
|  |         margin: 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text :global(a) { | ||||||
|  |         color: var(--accent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text :global(a.mention) { | ||||||
|  |         color: var(--accent); | ||||||
|  |         padding: 6px 6px; | ||||||
|  |         margin: -6px 0; | ||||||
|  |         background: var(--accent-bg); | ||||||
|  |         border-radius: 6px; | ||||||
|  |         text-decoration: none; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text :global(a.mention:hover) { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text :global(a.hashtag) { | ||||||
|  |         background-color: transparent; | ||||||
|  |         padding: 0; | ||||||
|  |         font-style: italic; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-text :global(.mention-avatar) { | ||||||
|  |         position: relative; | ||||||
|  |         top: 4px; | ||||||
|  |         height: 20px; | ||||||
|  |         margin-right: 4px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container { | ||||||
|  |         max-height: 540px; | ||||||
|  |         margin-top: 8px; | ||||||
|  |         display: grid; | ||||||
|  |         grid-gap: 8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container[data-count="1"] { | ||||||
|  |         grid-template-rows: 1fr; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container[data-count="2"] { | ||||||
|  |         grid-template-columns: 1fr 1fr; | ||||||
|  |         grid-template-rows: 1fr; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container[data-count="3"] { | ||||||
|  |         grid-template-columns: 1fr .5fr; | ||||||
|  |         grid-template-rows: 1fr 1fr; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container[data-count="4"] { | ||||||
|  |         grid-template-columns: 1fr 1fr; | ||||||
|  |         grid-template-rows: 1fr 1fr; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media { | ||||||
|  |         border-radius: 12px; | ||||||
|  |         background-color: #000; | ||||||
|  |         overflow: hidden; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media a { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 100%; | ||||||
|  |         display: block; | ||||||
|  |         cursor: zoom-in; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media a img { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 100%; | ||||||
|  |         display: block; | ||||||
|  |         object-fit: contain; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container > :nth-child(1) { | ||||||
|  |         grid-column: 1/2; | ||||||
|  |         grid-row: 1/2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container[data-count="3"] > :nth-child(1) { | ||||||
|  |         grid-row: 1/3; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container > :nth-child(2) { | ||||||
|  |         grid-column: 2/2; | ||||||
|  |         grid-row: 1/2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container > :nth-child(3) { | ||||||
|  |         grid-column: 1/2; | ||||||
|  |         grid-row: 2/2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container[data-count="3"] > :nth-child(3) { | ||||||
|  |         grid-column: 2/2; | ||||||
|  |         grid-row: 2/2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-media-container > :nth-child(4) { | ||||||
|  |         grid-column: 2/2; | ||||||
|  |         grid-row: 2/2; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										52
									
								
								src/post/BoostContext.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/post/BoostContext.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | ||||||
|  | <script> | ||||||
|  |     import { parse_text as parse_emojis } from '../emoji.js'; | ||||||
|  |     import { shorthand as short_time } from '../time.js'; | ||||||
|  | 
 | ||||||
|  |     export let post; | ||||||
|  | 
 | ||||||
|  |     let time_string = post.created_at.toLocaleString(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="post-context"> | ||||||
|  |     <span class="post-context-icon">🔁</span> | ||||||
|  |     <span class="post-context-action"> | ||||||
|  |         <a href="/{post.user.mention}">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> boosted this post. | ||||||
|  |     </span> | ||||||
|  |     <span class="post-context-time"> | ||||||
|  |         <time title="{time_string}">{short_time(post.created_at)}</time> | ||||||
|  |     </span> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .post-context { | ||||||
|  |         margin-bottom: 8px; | ||||||
|  |         padding-left: 58px; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |         align-items: center; | ||||||
|  |         color: var(--accent); | ||||||
|  |         opacity: .8; | ||||||
|  |         transition: opacity .1s; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-container:hover .post-context { | ||||||
|  |         opacity: 1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-context-icon { | ||||||
|  |         margin-right: 4px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-context a, | ||||||
|  |     .post-context a:visited { | ||||||
|  |         color: inherit; | ||||||
|  |         text-decoration: none; | ||||||
|  |     } | ||||||
|  |     .post-context a:hover { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-context-time { | ||||||
|  |         margin-left: auto; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										54
									
								
								src/post/FooterButton.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/post/FooterButton.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | ||||||
|  | <script> | ||||||
|  |     import { play_sound } from '../sound.js'; | ||||||
|  | 
 | ||||||
|  |     export let icon = "🔧"; | ||||||
|  |     export let type = "action"; | ||||||
|  |     export let label = "Action"; | ||||||
|  |     export let title = label; | ||||||
|  |     export let count = 0; | ||||||
|  |     export let sound = "default"; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <button | ||||||
|  |         type="button" | ||||||
|  |         class="{type}" | ||||||
|  |         aria-label="{label}" | ||||||
|  |         title="{title}" | ||||||
|  |         on:click={() => (play_sound(sound))}> | ||||||
|  |         <span>{@html icon}</span> | ||||||
|  |         {#if count} | ||||||
|  |             <span class="count">{count}</span> | ||||||
|  |         {/if} | ||||||
|  | </button> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     button { | ||||||
|  |         padding: 6px 8px; | ||||||
|  |         font-size: 1em; | ||||||
|  |         background: none; | ||||||
|  |         color: inherit; | ||||||
|  |         border: none; | ||||||
|  |         border-radius: 8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button.active { | ||||||
|  |         background: var(--accent); | ||||||
|  |         color: var(--bg0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button:hover { | ||||||
|  |         background: #8881; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button:active { | ||||||
|  |         background: #0001; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .count { | ||||||
|  |         opacity: .5; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button:hover .count { | ||||||
|  |         opacity: 1; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										79
									
								
								src/post/Header.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/post/Header.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | ||||||
|  | <script> | ||||||
|  |     import { parse_text as parse_emojis } from '../emoji.js'; | ||||||
|  |     import { shorthand as short_time } from '../time.js'; | ||||||
|  | 
 | ||||||
|  |     export let post; | ||||||
|  | 
 | ||||||
|  |     let time_string = post.created_at.toLocaleString(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="post-header-container"> | ||||||
|  |     <a href="/{post.user.mention}" class="post-avatar-container"> | ||||||
|  |         <img src={post.user.avatar_url} type={post.user.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> | ||||||
|  |     </a> | ||||||
|  |     <header class="post-header"> | ||||||
|  |         <div class="post-user-info"> | ||||||
|  |             <a href="/{post.user.mention}" class="name">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> | ||||||
|  |             <span class="username">{post.user.mention}</span> | ||||||
|  |         </div> | ||||||
|  |         <div class="post-info"> | ||||||
|  |             <a href={post.url} class="created-at"> | ||||||
|  |                 <time title={time_string}>{short_time(post.created_at)}</time> | ||||||
|  |             </a> | ||||||
|  |         </div> | ||||||
|  |     </header> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .post-header-container { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-header-container a, | ||||||
|  |     .post-header-container a:visited { | ||||||
|  |         color: inherit; | ||||||
|  |         text-decoration: none; | ||||||
|  |     } | ||||||
|  |     .post-header-container a:hover { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-avatar-container { | ||||||
|  |         margin-right: 12px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-avatar { | ||||||
|  |         border-radius: 8px; | ||||||
|  |         box-shadow: 2px 2px #0004; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-header { | ||||||
|  |         display: flex; | ||||||
|  |         flex-grow: 1; | ||||||
|  |         flex-direction: row; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-info { | ||||||
|  |         margin-left: auto; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-user-info a { | ||||||
|  |         display: block; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-user-info .name :global(.emoji) { | ||||||
|  |         position: relative; | ||||||
|  |         top: 4px; | ||||||
|  |         height: 26px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-user-info .username { | ||||||
|  |         opacity: .5; | ||||||
|  |         font-size: .9em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-info .created-at { | ||||||
|  |         font-size: .8em; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										79
									
								
								src/post/Post.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/post/Post.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | ||||||
|  | <script> | ||||||
|  |     import BoostContext from './BoostContext.svelte'; | ||||||
|  |     import ReplyContext from './ReplyContext.svelte'; | ||||||
|  |     import Header from './Header.svelte'; | ||||||
|  |     import Body from './Body.svelte'; | ||||||
|  |     import FooterButton from './FooterButton.svelte'; | ||||||
|  |     import { parse_one as parse_reaction } from '../emoji.js'; | ||||||
|  |     import { play_sound } from '../sound.js'; | ||||||
|  | 
 | ||||||
|  |     export let post; | ||||||
|  | 
 | ||||||
|  |     let post_context = undefined; | ||||||
|  |     let _post = post; | ||||||
|  |     let is_boost = false; | ||||||
|  |     if (_post.boost) { | ||||||
|  |         is_boost = true; | ||||||
|  |         post_context = _post; | ||||||
|  |         _post = _post.boost; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="post-container" aria-label={aria_label}> | ||||||
|  |     {#if _post.reply} | ||||||
|  |         <ReplyContext post={_post.reply} /> | ||||||
|  |     {/if} | ||||||
|  |     {#if is_boost && !post_context.text} | ||||||
|  |         <BoostContext post={post_context} /> | ||||||
|  |     {/if} | ||||||
|  |     <article class="post"> | ||||||
|  |         <Header post={_post} /> | ||||||
|  |         <Body post={_post} /> | ||||||
|  |         <footer class="post-footer"> | ||||||
|  |             <div class="post-reactions"> | ||||||
|  |                 {#each Object.keys(_post.reactions) as reaction} | ||||||
|  |                     <FooterButton icon={parse_reaction(reaction, _post.emojis)} type="reaction" bind:count={_post.reactions[reaction]} title={reaction} label="" /> | ||||||
|  |                 {/each} | ||||||
|  |             </div> | ||||||
|  |             <div class="post-actions"> | ||||||
|  |                 <FooterButton icon="🗨️" type="reply" label="Reply" bind:count={_post.reply_count} sound="post" /> | ||||||
|  |                 <FooterButton icon="🔁" type="boost" label="Boost" bind:count={_post.boost_count} sound="boost" /> | ||||||
|  |                 <FooterButton icon="⭐" type="favourite" label="Favourite" /> | ||||||
|  |                 <FooterButton icon="😃" type="react" label="React" /> | ||||||
|  |                 <FooterButton icon="🗣️" type="quote" label="Quote" /> | ||||||
|  |                 <FooterButton icon="🛠️" type="more" label="More" /> | ||||||
|  |             </div> | ||||||
|  |         </footer> | ||||||
|  |     </article> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .post-container { | ||||||
|  |         margin-top: 16px; | ||||||
|  |         padding: 28px 32px 20px 32px; | ||||||
|  |         border: 1px solid #8884; | ||||||
|  |         border-radius: 16px; | ||||||
|  |         background-color: var(--bg1); | ||||||
|  |         transition: background-color .1s; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-container:hover { | ||||||
|  |         background-color: var(--bg2); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-reactions { | ||||||
|  |         margin-top: 8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-actions { | ||||||
|  |         margin-top: 8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-container :global(.emoji) { | ||||||
|  |         position: relative; | ||||||
|  |         top: 6px; | ||||||
|  |         height: 26px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										130
									
								
								src/post/ReplyContext.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/post/ReplyContext.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,130 @@ | ||||||
|  | <script> | ||||||
|  |     import Header from './Header.svelte'; | ||||||
|  |     import Body from './Body.svelte'; | ||||||
|  |     import FooterButton from './FooterButton.svelte'; | ||||||
|  |     import Post from './Post.svelte'; | ||||||
|  |     import { parse_text as parse_emojis, parse_one as parse_reaction } from '../emoji.js'; | ||||||
|  |     import { shorthand as short_time } from '../time.js'; | ||||||
|  | 
 | ||||||
|  |     export let post; | ||||||
|  | 
 | ||||||
|  |     let time_string = post.created_at.toLocaleString(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <article class="post-reply"> | ||||||
|  |     <div class="post-reply-avatar-container"> | ||||||
|  |         <a href="/{post.user.mention}" class="post-avatar-container"> | ||||||
|  |             <img src={post.user.avatar_url} type={post.user.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> | ||||||
|  |         </a> | ||||||
|  |         <div class="line"> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="post-reply-main"> | ||||||
|  |         <div class="post-header-container"> | ||||||
|  |             <header class="post-header"> | ||||||
|  |                 <div class="post-user-info"> | ||||||
|  |                     <a href="/{post.user.mention}" class="name">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> | ||||||
|  |                     <span class="username">{post.user.mention}</span> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="post-info"> | ||||||
|  |                     <a href={post.url} class="created-at"> | ||||||
|  |                         <time title={time_string}>{short_time(post.created_at)}</time> | ||||||
|  |                     </a> | ||||||
|  |                 </div> | ||||||
|  |             </header> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <Body post={post} /> | ||||||
|  | 
 | ||||||
|  |         <footer class="post-footer"> | ||||||
|  |             <div class="post-reactions"> | ||||||
|  |                 {#each Object.keys(post.reactions) as reaction} | ||||||
|  |                     <FooterButton icon={parse_reaction(reaction, post.emojis)} type="reaction" bind:count={post.reactions[reaction]} title={reaction} label="" /> | ||||||
|  |                 {/each} | ||||||
|  |             </div> | ||||||
|  |             <div class="post-actions"> | ||||||
|  |                 <FooterButton icon="🗨️" type="reply" label="Reply" bind:count={post.reply_count} /> | ||||||
|  |                 <FooterButton icon="🔁" type="boost" label="Boost" bind:count={post.boost_count} /> | ||||||
|  |                 <FooterButton icon="⭐" type="favourite" label="Favourite" /> | ||||||
|  |                 <FooterButton icon="😃" type="react" label="React" /> | ||||||
|  |                 <FooterButton icon="🗣️" type="quote" label="Quote" /> | ||||||
|  |                 <FooterButton icon="🛠️" type="more" label="More" /> | ||||||
|  |             </div> | ||||||
|  |         </footer> | ||||||
|  |     </div> | ||||||
|  | </article> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .post-reply { | ||||||
|  |         padding-bottom: 24px; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-reply-avatar-container { | ||||||
|  |         margin-right: 12px; | ||||||
|  |         margin-bottom: -24px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-reply-avatar-container .line { | ||||||
|  |         position: relative; | ||||||
|  |         top: -4px; | ||||||
|  |         left: -1px; | ||||||
|  |         width: 50%; | ||||||
|  |         height: calc(100% - 48px); | ||||||
|  |         border-right: 2px solid #8888; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-reply-main { | ||||||
|  |         flex-grow: 1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-header-container { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-header-container a, | ||||||
|  |     .post-header-container a:visited { | ||||||
|  |         color: inherit; | ||||||
|  |         text-decoration: none; | ||||||
|  |     } | ||||||
|  |     .post-header-container a:hover { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-avatar { | ||||||
|  |         border-radius: 8px; | ||||||
|  |         box-shadow: 2px 2px #0004; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-header { | ||||||
|  |         display: flex; | ||||||
|  |         flex-grow: 1; | ||||||
|  |         flex-direction: row; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-info { | ||||||
|  |         margin-left: auto; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-user-info a { | ||||||
|  |         display: block; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-user-info .name :global(.emoji) { | ||||||
|  |         position: relative; | ||||||
|  |         top: 4px; | ||||||
|  |         max-height: 1.25em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-user-info .username { | ||||||
|  |         opacity: .5; | ||||||
|  |         font-size: .9em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-info .created-at { | ||||||
|  |         font-size: .8em; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										220
									
								
								src/post/post.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								src/post/post.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,220 @@ | ||||||
|  | import Instance from '../instance.js'; | ||||||
|  | import User from '../user/user.js'; | ||||||
|  | 
 | ||||||
|  | import { parse_one as parse_emoji } from '../emoji.js'; | ||||||
|  | 
 | ||||||
|  | let post_cache = Object; | ||||||
|  | 
 | ||||||
|  | export default class Post { | ||||||
|  |     id; | ||||||
|  |     created_at; | ||||||
|  |     user; | ||||||
|  |     text; | ||||||
|  |     warning; | ||||||
|  |     boost_count; | ||||||
|  |     reply_count; | ||||||
|  |     mentions; | ||||||
|  |     reactions; | ||||||
|  |     emojis; | ||||||
|  |     files; | ||||||
|  |     url; | ||||||
|  |     reply; | ||||||
|  |     boost; | ||||||
|  | 
 | ||||||
|  |     static resolve_id(id) { | ||||||
|  |         return post_cache[id] || null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static parse(data) { | ||||||
|  |         const instance = Instance.get_instance(); | ||||||
|  |         let post = null; | ||||||
|  |         switch (instance.type) { | ||||||
|  |             case Instance.types.ICESHRIMP: | ||||||
|  |                 post = Post.#parse_iceshrimp(data); | ||||||
|  |                 break; | ||||||
|  |             case Instance.types.MASTODON: | ||||||
|  |                 post = Post.#parse_mastodon(data); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         if (!post) { | ||||||
|  |             console.error("Error while parsing post data"); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         post_cache[post.id] = post; | ||||||
|  |         return post; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static #parse_iceshrimp(data) { | ||||||
|  |         let post = new Post() | ||||||
|  |         post.id = data.id; | ||||||
|  |         post.created_at = new Date(data.createdAt); | ||||||
|  |         post.user = User.parse(data.user); | ||||||
|  |         post.text = data.text; | ||||||
|  |         post.warning = data.cw; | ||||||
|  |         post.boost_count = data.renoteCount; | ||||||
|  |         post.reply_count = data.repliesCount; | ||||||
|  |         post.mentions = data.mentions; | ||||||
|  |         post.reactions = data.reactions; | ||||||
|  |         post.emojis = data.emojis; | ||||||
|  |         post.files = data.files; | ||||||
|  |         post.url = data.url; | ||||||
|  |         post.boost = data.renote ? Post.parse(data.renote) : null; | ||||||
|  |         post.reply = data.reply ? Post.parse(data.reply) : null; | ||||||
|  |         return post; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static #parse_mastodon(data) { | ||||||
|  |         let post = new Post() | ||||||
|  |         post.id = data.id; | ||||||
|  |         post.created_at = new Date(data.created_at); | ||||||
|  |         post.user = User.parse(data.account); | ||||||
|  |         post.text = data.content; | ||||||
|  |         post.warning = data.spoiler_text; | ||||||
|  |         post.boost_count = data.reblogs_count; | ||||||
|  |         post.reply_count = data.replies_count; | ||||||
|  |         post.mentions = data.mentions; | ||||||
|  |         post.reactions = data.reactions; | ||||||
|  |         post.emojis = data.emojis; | ||||||
|  |         post.files = data.media_attachments; | ||||||
|  |         post.url = data.url; | ||||||
|  |         post.boost = data.reblog ? Post.parse(data.reblog) : null; | ||||||
|  |         post.reply = data.in_reply_to_id ? Post.resolve_id(data.in_reply_to_id) : null; | ||||||
|  |         return post; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get rich_text() { | ||||||
|  |         let text = this.text; | ||||||
|  |         if (!text) return text; | ||||||
|  | 
 | ||||||
|  |         const markdown_tokens = [ | ||||||
|  |             { tag: "pre", token: "```" }, | ||||||
|  |             { tag: "code", token: "`" }, | ||||||
|  |             { tag: "strong", token: "**", regex: /\*{2}/g }, | ||||||
|  |             { tag: "strong", token: "__" }, | ||||||
|  |             { tag: "em", token: "*", regex: /\*/g }, | ||||||
|  |             { tag: "em", token: "_" }, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         let response = ""; | ||||||
|  |         let current; | ||||||
|  |         let index = 0; | ||||||
|  |         while (index < text.length) { | ||||||
|  |             let sample = text.substring(index); | ||||||
|  |             let allow_new = !current || !current.nostack; | ||||||
|  | 
 | ||||||
|  |             // handle newlines
 | ||||||
|  |             if (allow_new && sample.startsWith('\n')) { | ||||||
|  |                 response += "<br>"; | ||||||
|  |                 index++; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // handle mentions
 | ||||||
|  |             if (allow_new && sample.match(/@[a-z0-9-_.]+@[a-z0-9-_.]+/g)) { | ||||||
|  |                 // find end of the mention
 | ||||||
|  |                 let length = 1; | ||||||
|  |                 while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; | ||||||
|  |                 length++; // skim the middle @
 | ||||||
|  |                 while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; | ||||||
|  | 
 | ||||||
|  |                 let mention = text.substring(index, index + length); | ||||||
|  | 
 | ||||||
|  |                 // attempt to resolve mention to a user
 | ||||||
|  |                 let user = User.resolve_mention(mention); | ||||||
|  |                 if (user) { | ||||||
|  |                     const out = `<a href="/${user.mention}" class="mention">` + | ||||||
|  |                         `<img src="${user.avatar_url}" class="mention-avatar" width="20" height="20">` + | ||||||
|  |                         "@" + user.name + "</a>"; | ||||||
|  |                     if (current) current.text += out; | ||||||
|  |                     else response += out; | ||||||
|  |                 } else { | ||||||
|  |                     response += mention; | ||||||
|  |                 } | ||||||
|  |                 index += mention.length; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (Instance.get_instance().type !== Instance.types.MASTODON) { | ||||||
|  |                 // handle links
 | ||||||
|  |                 if (allow_new && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g)) { | ||||||
|  |                     // get length of link
 | ||||||
|  |                     let length = text.substring(index).search(/\s|$/g); | ||||||
|  |                     let url = text.substring(index, index + length); | ||||||
|  |                     let out = `<a href="${url}">${url}</a>`; | ||||||
|  |                     if (current) current.text += out; | ||||||
|  |                     else response += out; | ||||||
|  |                     index += length; | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // handle emojis
 | ||||||
|  |             if (allow_new && sample.startsWith(':')) { | ||||||
|  |                 // lookahead to next invalid emoji character
 | ||||||
|  |                 let look = sample.substring(1).search(/[^a-zA-Z0-9-_.]/g) + 1; | ||||||
|  |                 // if it's ':', we can parse it
 | ||||||
|  |                 if (look !== 0 && sample[look] === ':') { | ||||||
|  |                     let emoji_code = sample.substring(0, look + 1); | ||||||
|  |                     let out = parse_emoji(emoji_code, this.emojis); | ||||||
|  |                     if (current) current.text += out; | ||||||
|  |                     else response += out; | ||||||
|  |                     index += emoji_code.length; | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // handle markdown
 | ||||||
|  |             // TODO: handle misskey-flavoured markdown
 | ||||||
|  |             if (current) { | ||||||
|  |                 // try to pop stack
 | ||||||
|  |                 if (sample.startsWith(current.token)) { | ||||||
|  |                     index += current.token.length; | ||||||
|  |                     let out = `<${current.tag}>${current.text}</${current.tag}>`; | ||||||
|  |                     if (current.token === '```') | ||||||
|  |                         out = `<code><pre>${current.text}</pre></code>`; | ||||||
|  |                     if (current.parent) current.parent.text += out; | ||||||
|  |                     else response += out; | ||||||
|  |                     current = current.parent; | ||||||
|  |                 } else { | ||||||
|  |                     current.text += sample[0]; | ||||||
|  |                     index++; | ||||||
|  |                 } | ||||||
|  |             } else if (allow_new) { | ||||||
|  |                 // can we add to stack?
 | ||||||
|  |                 let pushed = false; | ||||||
|  |                 for (let i = 0; i < markdown_tokens.length; i++) { | ||||||
|  |                     let item = markdown_tokens[i]; | ||||||
|  |                     if (sample.startsWith(item.token)) { | ||||||
|  |                         let new_current = { | ||||||
|  |                             token: item.token, | ||||||
|  |                             tag: item.tag, | ||||||
|  |                             text: "", | ||||||
|  |                             parent: current, | ||||||
|  |                         }; | ||||||
|  |                         if (item.token === '```' || item.token === '`') new_current.nostack = true; | ||||||
|  |                         current = new_current; | ||||||
|  |                         pushed = true; | ||||||
|  |                         index += current.token.length; | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 if (!pushed) { | ||||||
|  |                     response += sample[0]; | ||||||
|  |                     index++; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // destroy the remaining stack
 | ||||||
|  |         while (current) { | ||||||
|  |             let out = current.token + current.text; | ||||||
|  |             if (current.parent) current.parent.text += out; | ||||||
|  |             else response += out; | ||||||
|  |             current = current.parent; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								src/sound.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/sound.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | const sounds = { | ||||||
|  |     "default": new Audio("sound/log.ogg"), | ||||||
|  |     "post": new Audio("sound/success.ogg"), | ||||||
|  |     "boost": new Audio("sound/hello.ogg"), | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export function play_sound(name) { | ||||||
|  |     if (!name) name = "default"; | ||||||
|  |     const sound = sounds[name]; | ||||||
|  |     if (!sound) { | ||||||
|  |         console.warn(`Attempted to play sound "${name}", which does not exist!`); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     sound.pause(); | ||||||
|  |     sound.currentTime = 0; | ||||||
|  |     sound.play(); | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								src/time.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/time.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | const denoms = [ | ||||||
|  |     { unit: 's', min: 0 }, | ||||||
|  |     { unit: 'm', min: 60 }, | ||||||
|  |     { unit: 'h', min: 60 }, | ||||||
|  |     { unit: 'd', min: 24 }, | ||||||
|  |     { unit: 'w', min: 7 }, | ||||||
|  |     { unit: 'y', min: 52 }, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | export function shorthand(date) { | ||||||
|  |     let value = (new Date() - date) / 1000; | ||||||
|  |     let unit = 's'; | ||||||
|  |     let index = 0; | ||||||
|  |     while (index < denoms.length - 1) { | ||||||
|  |         if (value < denoms[index + 1].min) break; | ||||||
|  |         index++ | ||||||
|  |         value /= denoms[index].min; | ||||||
|  |         unit = denoms[index].unit; | ||||||
|  |     } | ||||||
|  |     if (value > 0) | ||||||
|  |         return Math.floor(value) + unit + " ago"; | ||||||
|  |     return "in " + Math.floor(value) + unit; | ||||||
|  | } | ||||||
							
								
								
									
										83
									
								
								src/user/user.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/user/user.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,83 @@ | ||||||
|  | import Instance from '../instance.js'; | ||||||
|  | import Emoji from '../emoji.js'; | ||||||
|  | 
 | ||||||
|  | let user_cache = Object; | ||||||
|  | 
 | ||||||
|  | export default class User { | ||||||
|  |     id; | ||||||
|  |     nickname; | ||||||
|  |     username; | ||||||
|  |     host; | ||||||
|  |     avatar_url; | ||||||
|  |     emojis; | ||||||
|  | 
 | ||||||
|  |     static resolve_id(id) { | ||||||
|  |         return user_cache[id]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static resolve_mention(mention) { | ||||||
|  |         for (let i = 0; i < Object.keys(user_cache).length; i++) { | ||||||
|  |             let user = user_cache[Object.keys(user_cache)[i]]; | ||||||
|  |             if (user.mention === mention) return user; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static parse(data) { | ||||||
|  |         const instance = Instance.get_instance(); | ||||||
|  |         let user = null; | ||||||
|  |         switch (instance.type) { | ||||||
|  |             case Instance.types.ICESHRIMP: | ||||||
|  |                 user = User.#parse_iceshrimp(data); | ||||||
|  |                 break; | ||||||
|  |             case Instance.types.MASTODON: | ||||||
|  |                 user = User.#parse_mastodon(data); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         if (!user) { | ||||||
|  |             console.error("Error while parsing user data"); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         user_cache[user.id] = user; | ||||||
|  |         return user; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static #parse_iceshrimp(data) { | ||||||
|  |         let user = new User(); | ||||||
|  |         user.id = data.id; | ||||||
|  |         user.nickname = data.name; | ||||||
|  |         user.username = data.username; | ||||||
|  |         user.host = data.host || Instance.get_instance().host; | ||||||
|  |         user.avatar_url = data.avatarUrl; | ||||||
|  |         user.emojis = []; | ||||||
|  |         data.emojis.forEach(emoji => { | ||||||
|  |             user.emojis.push(Emoji.parse(emoji, user.host)); | ||||||
|  |         }); | ||||||
|  |         return user; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static #parse_mastodon(data) { | ||||||
|  |         let user = new User(); | ||||||
|  |         user.id = data.id; | ||||||
|  |         user.nickname = data.display_name; | ||||||
|  |         user.username = data.username; | ||||||
|  |         user.host = data.acct.search('@') ? data.acct.substring(data.acct.search('@') + 1) : instance.host; | ||||||
|  |         user.avatar_url = data.avatar; | ||||||
|  |         user.emojis = []; | ||||||
|  |         data.emojis.forEach(emoji => { | ||||||
|  |             user.emojis.push(Emoji.parse(emoji, user.host)); | ||||||
|  |         }); | ||||||
|  |         return user; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get name() { | ||||||
|  |         return this.nickname || this.username; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get mention() { | ||||||
|  |         let res = "@" + this.username; | ||||||
|  |         if (this.host) res += "@" + this.host; | ||||||
|  |         return res; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								svelte.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								svelte.config.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |     // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
 | ||||||
|  |     // for more information about preprocessors
 | ||||||
|  |     preprocess: vitePreprocess(), | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								vite.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								vite.config.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | import { defineConfig } from 'vite' | ||||||
|  | import { svelte } from '@sveltejs/vite-plugin-svelte' | ||||||
|  | 
 | ||||||
|  | // https://vitejs.dev/config/
 | ||||||
|  | export default defineConfig({ | ||||||
|  |     plugins: [svelte()], | ||||||
|  | }) | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue