Compare commits
	
		
			53 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4bad86822f | |||
| 19be3779e3 | |||
| cd5dc461f6 | |||
| 389f8dcaff | |||
| 671c45085d | |||
| 0f17d92314 | |||
| f897f812c3 | |||
| 374207c594 | |||
| c11069b187 | |||
| 1d4b121ea5 | |||
| daaa819e6c | |||
| ea1a492dc0 | |||
| 3b8ca902f1 | |||
| 99def58c8b | |||
| c51a0b1e5d | |||
| 7db5ec7fae | |||
| 1b25e56d0a | |||
| 2d7c346577 | |||
|   | 8e9fb6598e | ||
| faab37a53b | |||
| 7ed8ebf6e5 | |||
| 876e221400 | |||
| e3586f4eec | |||
| 563541d0e6 | |||
| 00277741a8 | |||
| 6f446fd871 | |||
| 0dd903a4eb | |||
| 3d1f38bdce | |||
| fe9d216552 | |||
| a5a066be3d | |||
| 22d6c5b90a | |||
| 455679a525 | |||
|   | f6901085f5 | ||
| d8efaccb30 | |||
| 77665702b7 | |||
| d0163ee094 | |||
| 449a11ee55 | |||
| 667b11f2f4 | |||
| 7752585488 | |||
| b170a532f6 | |||
| b74b19cc73 | |||
| 30f3aadeaa | |||
| f4709a232d | |||
| e326ac858e | |||
| a820b40318 | |||
| a1c1b5f4d0 | |||
| 970590497f | |||
| b295b6f03a | |||
| f771866a09 | |||
| c402f329a7 | |||
| 0a563e6121 | |||
| 8d9c3cc4fe | |||
| a1ec63b7ec | 
					 49 changed files with 2140 additions and 2028 deletions
				
			
		
							
								
								
									
										10
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
										
									
									
									
								
							|  | @ -35,11 +35,13 @@ will likely be incompatible, and the web console will get very upset. | |||
| 
 | ||||
| ## try it out! | ||||
| 
 | ||||
| - `git clone` this repo | ||||
| - `npm install` the dependencies | ||||
| - `npm run dev` to spin up the dev environment | ||||
| campfire uses [bun](https://bun.sh/) as a package manager and runtime. | ||||
| 
 | ||||
| if you wish to run this in production, you need only `npm run build` and | ||||
| - `git clone` this repo | ||||
| - `bun install` the dependencies | ||||
| - `bun run dev` to spin up the dev environment | ||||
| 
 | ||||
| if you wish to run this in production, you need only `bun run build` and | ||||
| place the static files somewhere accessible by a static webhost, such as | ||||
| nginx or apache! **note:** your web server should attempt to reach | ||||
| `/fallback.html` before erroring out. | ||||
|  |  | |||
							
								
								
									
										246
									
								
								bun.lock
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								bun.lock
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,246 @@ | |||
| { | ||||
|   "lockfileVersion": 1, | ||||
|   "workspaces": { | ||||
|     "": { | ||||
|       "name": "campfire-client", | ||||
|       "devDependencies": { | ||||
|         "@poppanator/sveltekit-svg": "^4.2.1", | ||||
|         "@sveltejs/adapter-auto": "^3.2.2", | ||||
|         "@sveltejs/adapter-static": "^3.0.2", | ||||
|         "@sveltejs/kit": "^2.16.0", | ||||
|         "@sveltejs/vite-plugin-svelte": "^3.1.1", | ||||
|         "svelte": "^4.2.18", | ||||
|         "vite": "^5.3.1", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   "packages": { | ||||
|     "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], | ||||
| 
 | ||||
|     "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], | ||||
| 
 | ||||
|     "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], | ||||
| 
 | ||||
|     "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], | ||||
| 
 | ||||
|     "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], | ||||
| 
 | ||||
|     "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], | ||||
| 
 | ||||
|     "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], | ||||
| 
 | ||||
|     "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], | ||||
| 
 | ||||
|     "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], | ||||
| 
 | ||||
|     "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], | ||||
| 
 | ||||
|     "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], | ||||
| 
 | ||||
|     "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], | ||||
| 
 | ||||
|     "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], | ||||
| 
 | ||||
|     "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], | ||||
| 
 | ||||
|     "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], | ||||
| 
 | ||||
|     "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], | ||||
| 
 | ||||
|     "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], | ||||
| 
 | ||||
|     "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], | ||||
| 
 | ||||
|     "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], | ||||
| 
 | ||||
|     "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], | ||||
| 
 | ||||
|     "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], | ||||
| 
 | ||||
|     "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], | ||||
| 
 | ||||
|     "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], | ||||
| 
 | ||||
|     "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], | ||||
| 
 | ||||
|     "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], | ||||
| 
 | ||||
|     "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], | ||||
| 
 | ||||
|     "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], | ||||
| 
 | ||||
|     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], | ||||
| 
 | ||||
|     "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], | ||||
| 
 | ||||
|     "@poppanator/sveltekit-svg": ["@poppanator/sveltekit-svg@4.2.1", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "svelte": ">=4.x", "svgo": ">=3.x", "vite": ">=4.x" } }, "sha512-w7jl4EVOOF+X+uv2BEUiMDJwds+GfbczwGpcS0+rsjIsKYmqmwMi4ts3bVZR9ZvdFHWy5rS84U+pSBClz6cbBg=="], | ||||
| 
 | ||||
|     "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], | ||||
| 
 | ||||
|     "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.0", "", { "os": "android", "cpu": "arm" }, "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg=="], | ||||
| 
 | ||||
|     "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ=="], | ||||
| 
 | ||||
|     "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg=="], | ||||
| 
 | ||||
|     "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA=="], | ||||
| 
 | ||||
|     "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA=="], | ||||
| 
 | ||||
|     "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw=="], | ||||
| 
 | ||||
|     "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ=="], | ||||
| 
 | ||||
|     "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ=="], | ||||
| 
 | ||||
|     "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug=="], | ||||
| 
 | ||||
|     "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.0", "", { "os": "win32", "cpu": "x64" }, "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA=="], | ||||
| 
 | ||||
|     "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], | ||||
| 
 | ||||
|     "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@3.3.1", "", { "dependencies": { "import-meta-resolve": "^4.1.0" }, "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ=="], | ||||
| 
 | ||||
|     "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="], | ||||
| 
 | ||||
|     "@sveltejs/kit": ["@sveltejs/kit@2.22.5", "", { "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-l5i+LcDaoymD2mg5ziptnHmzzF79+c9twJiDoLWAPKq7afMEe4mvGesJ+LVtm33A92mLzd2KUHgtGSqTrvfkvg=="], | ||||
| 
 | ||||
|     "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@3.1.2", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", "debug": "^4.3.4", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.10", "svelte-hmr": "^0.16.0", "vitefu": "^0.2.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA=="], | ||||
| 
 | ||||
|     "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@2.1.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="], | ||||
| 
 | ||||
|     "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], | ||||
| 
 | ||||
|     "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], | ||||
| 
 | ||||
|     "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], | ||||
| 
 | ||||
|     "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], | ||||
| 
 | ||||
|     "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], | ||||
| 
 | ||||
|     "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], | ||||
| 
 | ||||
|     "code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="], | ||||
| 
 | ||||
|     "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], | ||||
| 
 | ||||
|     "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], | ||||
| 
 | ||||
|     "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], | ||||
| 
 | ||||
|     "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], | ||||
| 
 | ||||
|     "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], | ||||
| 
 | ||||
|     "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], | ||||
| 
 | ||||
|     "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], | ||||
| 
 | ||||
|     "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], | ||||
| 
 | ||||
|     "devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="], | ||||
| 
 | ||||
|     "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], | ||||
| 
 | ||||
|     "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], | ||||
| 
 | ||||
|     "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], | ||||
| 
 | ||||
|     "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], | ||||
| 
 | ||||
|     "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], | ||||
| 
 | ||||
|     "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], | ||||
| 
 | ||||
|     "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], | ||||
| 
 | ||||
|     "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], | ||||
| 
 | ||||
|     "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | ||||
| 
 | ||||
|     "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="], | ||||
| 
 | ||||
|     "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], | ||||
| 
 | ||||
|     "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], | ||||
| 
 | ||||
|     "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], | ||||
| 
 | ||||
|     "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], | ||||
| 
 | ||||
|     "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], | ||||
| 
 | ||||
|     "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], | ||||
| 
 | ||||
|     "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], | ||||
| 
 | ||||
|     "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], | ||||
| 
 | ||||
|     "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], | ||||
| 
 | ||||
|     "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], | ||||
| 
 | ||||
|     "periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="], | ||||
| 
 | ||||
|     "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], | ||||
| 
 | ||||
|     "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], | ||||
| 
 | ||||
|     "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], | ||||
| 
 | ||||
|     "rollup": ["rollup@4.45.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.0", "@rollup/rollup-android-arm64": "4.45.0", "@rollup/rollup-darwin-arm64": "4.45.0", "@rollup/rollup-darwin-x64": "4.45.0", "@rollup/rollup-freebsd-arm64": "4.45.0", "@rollup/rollup-freebsd-x64": "4.45.0", "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", "@rollup/rollup-linux-arm-musleabihf": "4.45.0", "@rollup/rollup-linux-arm64-gnu": "4.45.0", "@rollup/rollup-linux-arm64-musl": "4.45.0", "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", "@rollup/rollup-linux-riscv64-gnu": "4.45.0", "@rollup/rollup-linux-riscv64-musl": "4.45.0", "@rollup/rollup-linux-s390x-gnu": "4.45.0", "@rollup/rollup-linux-x64-gnu": "4.45.0", "@rollup/rollup-linux-x64-musl": "4.45.0", "@rollup/rollup-win32-arm64-msvc": "4.45.0", "@rollup/rollup-win32-ia32-msvc": "4.45.0", "@rollup/rollup-win32-x64-msvc": "4.45.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A=="], | ||||
| 
 | ||||
|     "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], | ||||
| 
 | ||||
|     "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], | ||||
| 
 | ||||
|     "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], | ||||
| 
 | ||||
|     "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], | ||||
| 
 | ||||
|     "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | ||||
| 
 | ||||
|     "svelte": ["svelte@4.2.20", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q=="], | ||||
| 
 | ||||
|     "svelte-hmr": ["svelte-hmr@0.16.0", "", { "peerDependencies": { "svelte": "^3.19.0 || ^4.0.0" } }, "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA=="], | ||||
| 
 | ||||
|     "svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="], | ||||
| 
 | ||||
|     "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], | ||||
| 
 | ||||
|     "vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="], | ||||
| 
 | ||||
|     "vitefu": ["vitefu@0.2.5", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["vite"] }, "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q=="], | ||||
| 
 | ||||
|     "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], | ||||
| 
 | ||||
|     "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], | ||||
| 
 | ||||
|     "svgo/css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], | ||||
| 
 | ||||
|     "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], | ||||
| 
 | ||||
|     "svgo/css-tree/mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], | ||||
|   } | ||||
| } | ||||
							
								
								
									
										1664
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1664
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1,19 +1,19 @@ | |||
| @import url("../font/inter/inter.css"); | ||||
| @import url("./font/inter/inter.css"); | ||||
| 
 | ||||
| :root { | ||||
|     --bg-1000: #fff6de; | ||||
|     --bg-900: #f9f1db; | ||||
|     --bg-800: #f1e8cf; | ||||
|     --bg-700: #d2c9b1; | ||||
|     --bg-600: #f0f6c2; | ||||
|     --accent: #8d9936; | ||||
|     --bg-1000: #fffcf7; | ||||
|     --bg-900: #faf4e4; | ||||
|     --bg-800: #f2e8d7; | ||||
|     --bg-700: #d9ccad; | ||||
|     --bg-600: #edf5ba; | ||||
|     --accent: #92a40a; | ||||
|     --text: #322e1f; | ||||
| } | ||||
| 
 | ||||
| @media (prefers-color-scheme: dark) { | ||||
|     :root { | ||||
|         --bg-1000: #141016; | ||||
|         --bg-900: #1B141E; | ||||
|         --bg-1000: #0b090c; | ||||
|         --bg-900: #120d14; | ||||
|         --bg-800: #2A202F; | ||||
|         --bg-700: #443749; | ||||
|         --bg-600: #513D60; | ||||
|  | @ -31,10 +31,6 @@ | |||
|     } | ||||
| } | ||||
| 
 | ||||
| @supports (font-variation-settings: normal) { | ||||
|     body { font-family: InterVariable, sans-serif; } | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|  | @ -48,6 +44,12 @@ body { | |||
|     box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| @supports (font-variation-settings: normal) { | ||||
|     body { | ||||
|         font-family: InterVariable, sans-serif; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|     color: var(--accent); | ||||
|     text-decoration: none; | ||||
|  | @ -71,7 +73,7 @@ header, #widgets { | |||
| } | ||||
| 
 | ||||
| main { | ||||
|     width: 732px; | ||||
|     width: 700px; | ||||
| } | ||||
| 
 | ||||
| img.emoji { | ||||
|  | @ -79,6 +81,10 @@ img.emoji { | |||
|     margin: -.2em 0; | ||||
| } | ||||
| 
 | ||||
| hr { | ||||
|     border-color: color-mix(in srgb, transparent, var(--accent) 50%); | ||||
| } | ||||
| 
 | ||||
| .throb { | ||||
|     animation: .25s throb alternate infinite ease-in; | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/img/icons/bot.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/img/icons/bot.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32"> | ||||
|   <path d="M22 14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V16a2 2 0 0 1 2-2h14Zm-7 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3.293-5.707a1 1 0 0 0-1.338-.068l-.076.068-2 2a1 1 0 1 0 1.414 1.414L11 19.414l1.293 1.293.076.068a1 1 0 0 0 1.406-1.406l-.068-.076-2-2Zm8 0a1 1 0 0 0-1.338-.068l-.076.068-2 2a1 1 0 1 0 1.414 1.414L19 19.414l1.293 1.293.076.068a1 1 0 0 0 1.406-1.406l-.068-.076-2-2Z"/> | ||||
|   <rect width="4" height="10" x="4" y="16" rx="2"/> | ||||
|   <rect width="4" height="10" x="22" y="16" rx="2"/> | ||||
|   <path d="M14 9h2v5h-2V9Z"/> | ||||
|   <circle cx="15" cy="9" r="2"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 646 B | 
							
								
								
									
										3
									
								
								src/img/icons/cross.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/img/icons/cross.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32"> | ||||
|   <path d="M20.763 7.627a2 2 0 1 1 2.908 2.746l-5.236 5.545 5.547 5.236.141.149a2 2 0 0 1-2.887 2.76l-5.546-5.237-5.236 5.547a2 2 0 1 1-2.908-2.746l5.236-5.546-5.546-5.235-.142-.149a2 2 0 0 1 2.731-2.893l.157.133 5.545 5.236 5.236-5.546Z"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 329 B | 
							
								
								
									
										4
									
								
								src/img/icons/lock.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/img/icons/lock.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32"> | ||||
|   <rect width="18" height="13" x="7" y="15" rx="2"/> | ||||
|   <path d="M16 7c1.608 0 2.895.534 3.873 1.382.95.822 1.535 1.875 1.902 2.83.369.96.547 1.898.634 2.582a11.746 11.746 0 0 1 .088 1.095l.002.074v.036l-3 .002v-.036c-.002-.034-.003-.09-.008-.162a8.703 8.703 0 0 0-.057-.628 8.303 8.303 0 0 0-.46-1.887c-.257-.67-.608-1.242-1.066-1.639C17.48 10.28 16.892 10 16 10c-.892 0-1.48.278-1.909.65-.457.396-.809.968-1.066 1.638a8.304 8.304 0 0 0-.46 1.887 8.799 8.799 0 0 0-.065.79v.037L9.5 15v-.036l.001-.074c.002-.061.004-.146.01-.25.011-.209.034-.5.078-.845.088-.684.267-1.622.636-2.582.367-.955.952-2.008 1.901-2.83C13.104 7.534 14.392 7 16 7Z"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 730 B | 
							
								
								
									
										3
									
								
								src/img/icons/tick.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/img/icons/tick.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32"> | ||||
|   <path d="M24.546 8.627a2 2 0 0 1 2.908 2.746l-.03.032-.03.032-.03.032-.03.032-.06.064-.06.063-.12.126c-.036.04-.02.023-.058.062l-.03.032-.059.062-.029.03-.059.063-.058.062-.03.03-.028.032-.116.122-.028.03-.03.032c-.097.103.04-.044-.057.06l-.453.48-.223.236-.11.117-.11.116-.856.907-.052.055-.053.056-.31.328-.051.055-.026.027-.026.027c-.114.122.064-.067-.05.054l-.026.027-.025.027-.051.055-.026.026-.025.027-.102.108-.05.052-.026.028-.024.026-.1.107-.051.052-.15.159-.024.026-.026.026-.049.053-.1.105-.023.026-.025.026-.025.027-.025.025-.097.104-.099.104-.024.026-.025.026-.146.155-.048.052-.049.05-.097.103-.049.052-.047.05-.097.103-.096.102c-.09.095.042-.045-.049.05l-.023.026-.024.025-.192.203-.023.026-.024.024-.024.026-.024.025-.38.403-.095.1-.095.1c-.04.043-.006.008-.047.05l-.023.026-.025.025-.093.1-.19.2-.023.025-.024.025-.046.05-.19.2-.047.05-.047.05-.19.2-.023.025-.023.025c-.084.089.036-.038-.048.05l-.047.05-.048.05a59.55 59.55 0 0 1-.094.1c-.104.11.057-.059-.047.051l-.048.05-.047.05-.023.025-.025.025c-.058.062.011-.01-.047.051-.042.046-.005.005-.048.05l-.094.1-.025.026-.023.025-.048.051-.048.05-.335.355-1.572 1.666-.102.107-.025.027-.025.027-.026.027-.025.026-.102.109-.026.027-.05.055-.027.026-.051.055-.026.027-.026.027-.103.11c-.033.035-.02.02-.052.056l-.053.054-.025.028-.053.055-.026.029-.105.11c-.052.056 0 .002-.053.056l-.026.029-.026.027-.027.028-.027.029-.053.056-.106.113c-.066.07.012-.014-.054.056l-.026.029-.028.028-.107.114c-.076.08-.032.035-.108.116l-.11.115-.11.116-.027.03-.083.087-.222.236-.226.238-.113.12-.03.031-.084.091-.03.03-.028.03-.03.03-.027.032-.06.06c-.123.132.067-.069-.057.062l-.029.031-.03.03-.028.032-.058.062-.03.03-.029.032-.03.031-.059.063-.06.062c-.09.097-.026.03-.117.126l-.24.255a2.002 2.002 0 0 1-2.91 0l-5.782-6.125-.133-.156a2 2 0 0 1 3.041-2.59l4.328 4.584.164-.173c.057-.06 0 .002.056-.058l.055-.06.054-.057.029-.029.027-.03.108-.114.028-.029.027-.03.027-.027.027-.03.054-.056.027-.029.027-.029.027-.028.027-.029.053-.056.027-.029.026-.028.054-.057.053-.055c.095-.1-.042.044.053-.057l.21-.223.21-.22.103-.11.05-.055.027-.027.05-.054.027-.028.051-.054.103-.108.203-.216.2-.212.1-.106.026-.026.024-.027.026-.026.024-.027.025-.026.05-.053.025-.026.098-.105.05-.052.049-.052.099-.104.39-.414c.107-.113-.01.012.097-.102.098-.103-.05.052.048-.052l.049-.05.095-.103.097-.102.048-.05.023-.026.025-.025.191-.203.024-.026.024-.024.023-.026.025-.025.38-.403.094-.1.095-.1.047-.05.024-.026.024-.025.047-.05.048-.05.188-.2.023-.025.025-.025.047-.05.047-.05.047-.05.094-.1.048-.05.047-.05.188-.2.025-.025.023-.025.047-.05.048-.05.047-.05.095-.1c.067-.072-.02.02.046-.051.05-.052 0 .002.048-.05l.048-.05.024-.025.023-.025c.054-.058.04-.044.095-.101l.096-.1.023-.026.024-.025.047-.051.025-.025.023-.025.096-.101.023-.026.025-.025.191-.203 1.572-1.666.026-.027.025-.026.152-.161.103-.109.05-.054.027-.028.026-.026.206-.22.052-.055.052-.054.052-.056.264-.278.212-.226.053-.056.054-.057.055-.058.053-.056.109-.115.109-.116.055-.058.027-.029.027-.029.11-.117.223-.236.454-.48c.046-.05.01-.011.057-.06l.058-.062.115-.122.03-.031.029-.03.087-.094.03-.03.117-.126.06-.062.029-.031.03-.033.06-.062.06-.064.03-.032.029-.031.06-.064.06-.064Z"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 3.2 KiB | 
							
								
								
									
										151
									
								
								src/lang/de_DE.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/lang/de_DE.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,151 @@ | |||
| { | ||||
|     "compose_placeholders": [ | ||||
|         "Was geht, %1?", | ||||
|         "Hau raus!", | ||||
|         "Leiste einen Beitrag!", | ||||
|         "Ich liebe es zu Posten!", | ||||
|         "Ein neuer Tag, ein neuer %1 post!" | ||||
|     ], | ||||
| 
 | ||||
|     "login": { | ||||
|         "welcome": "Willkommen, Fediverse-Nutzer!", | ||||
|         "enter_domain": "Bitte die Server-Domäne eingeben.", | ||||
|         "experimental": "Diese Software ist noch\n<strong><em>experimentele Software</em></strong>;\nFunktionen können jederzeit Probleme aufweisen!\n<br>\nWenn dies kein Problem ist, Willkommen am Bord!", | ||||
|         "button": "Einloggen", | ||||
|         "error": { | ||||
|             "no_domain": "Bitte eine valide Server-Domäne eingeben.", | ||||
|             "connection_failed": "Verbindung zum Server fehlgeschlagen! \nÜberprüfe die Browser-Konsole!", | ||||
|             "create_app": "Fehler beim erstellen einer App zum Server." | ||||
|         }, | ||||
|         "made_with_tagline": "Gemacht mit Liebe ❤ von <a href=\"https://bliss.town\">bliss town</a>" | ||||
|     }, | ||||
| 
 | ||||
|     "navigation": { | ||||
|         "timeline": "Timeline", | ||||
|         "notifications": "Benachrichtigungen", | ||||
|         "follow_requests": "Follower-Anfragen", | ||||
|         "explore": "Entdecken", | ||||
|         "lists": "Listen", | ||||
| 
 | ||||
|         "favourites": "Favoriten", | ||||
|         "bookmarks": "Lesezeichen", | ||||
|         "hashtags": "Hashtags", | ||||
| 
 | ||||
|         "profile_information": "Profil Informationen", | ||||
|         "settings": "Einstellungen", | ||||
|         "log_out": "Ausloggen", | ||||
| 
 | ||||
|         "back": "Zurück" | ||||
|     }, | ||||
| 
 | ||||
|     "follow_requests": { | ||||
|         "none": "Gerade keine Follower-Anfragen für dich!" | ||||
|     }, | ||||
| 
 | ||||
|     "timeline": { | ||||
|         "home": "Home-Feed", | ||||
|         "local": "Lokaler-Feed", | ||||
|         "federated": "Federations-Feed", | ||||
|         "fetching": "Feed wird geladen..." | ||||
|     }, | ||||
| 
 | ||||
|     "notification": { | ||||
|         "and_others": "und <strong>%1</strong> andere", | ||||
|         "mention": "%1 hat dich erwähnt.", | ||||
|         "reblog": "%1 hat deinen Post geboosted.", | ||||
|         "reaction": "%1 hat auf deinen Poste reagiert.", | ||||
|         "follow": "%1 hat die gefolgt.", | ||||
|         "follow_request": "%1 hat angefragt dir zu folgen.", | ||||
|         "favourite": "%1 hat deinen Post favorisiert.", | ||||
|         "poll": "%1's Umfrage wurde beendet.", | ||||
|         "update": "%1 hat deren Post bearbeitet.", | ||||
|         "default": "%1 hat dich angestupst!", | ||||
|         "fetching": "Benachrichtigungen werden geladen..." | ||||
|     }, | ||||
| 
 | ||||
|     "post": { | ||||
|         "loading": "Post wird geladen...", | ||||
|         "pinned": "📌 Angepinnter Post", | ||||
|         "by": "Post von %1", | ||||
|         "boosted": "%1 hat diesen Post geboosted.", | ||||
|         "actions": { | ||||
|             "reply": "Antworten", | ||||
|             "boost": "Boost", | ||||
|             "favourite": "Favorisieren", | ||||
|             "quote": "Zitieren", | ||||
|             "react": "Reagieren", | ||||
|             "more": "Mehr", | ||||
|             "delete": "Löschen" | ||||
|         }, | ||||
|         "warning": { | ||||
|             "placeholder": "Inhaltswarnung", | ||||
|             "show": "(Klicken zum Anzeigen)", | ||||
|             "hide": "(Klicken zum Verbergen)" | ||||
|         }, | ||||
|         "visibility": { | ||||
|             "public": "Öffentlich", | ||||
|             "unlisted": "Nicht gelistet", | ||||
|             "follow_only": "Nur Follower", | ||||
|             "private": "Privat", | ||||
|             "direct": "Direktnachricht" | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     "profile": { | ||||
|         "locked": "Dies ist ein privates Profil.", | ||||
|         "bot": "Dies ist ein automatiesiertes Profil.", | ||||
|         "followers": "Folgen", | ||||
|         "following": "Gefolgt", | ||||
|         "follow": "Folgen", | ||||
|         "home_instance": "Auf Hauptinstanz ansehen", | ||||
|         "more": "Mehr", | ||||
|         "posts": "Posts", | ||||
|         "replies": "Antworten", | ||||
|         "media": "Medien", | ||||
|         "loading": "Profil wird geladen..." | ||||
|     }, | ||||
| 
 | ||||
|     "logs": { | ||||
|         "logged_in": "Eingeloggt als %1", | ||||
|         "server_detected": "Server entdeckt als %1 (%2) mit Funktionen: {%3}}", | ||||
|         "server_unsupported": "Server %1 wird nicht unterstützt (%2). Funktionen können nicht garantiert werden und dein Haus wird vermutlich in Flammen aufgehen", | ||||
|         "no_hostname": "Versucht mit Server ohne Hostnamen zu verbinden", | ||||
|         "no_https": "Verbindung zu einem unsicheren Server feige verweigert.“", | ||||
|         "connection_failed": "Verbindung zu %1 fehlgeschlagen", | ||||
|         "post_fetch_failed": "Fehler beim Laden des Posts", | ||||
|         "post_fetch_failed_id": "Fehler beim Laden des Posts %1", | ||||
|         "post_parse_failed": "Fehler beim Analysieren des Posts", | ||||
|         "post_parse_failed_id": "Fehler beim Analysieren des Posts %1", | ||||
|         "profile_fetch_failed": "Fehler beim Laden des Profils", | ||||
|         "profile_fetch_failed_id": "Fehler beim Laden des Profils %1", | ||||
|         "token_revoke_failed": "Token widerrufen fehlgeschlagen! Daten werden trotzdem gedumpt", | ||||
|         "sound_does_not_exist": "Versucht Sound \"%1\" abzuspielen, aber es existiert nicht!", | ||||
|         "account_data_empty": "Profil Daten wurden analysiert, aber keine Daten wurden gefunden!", | ||||
|         "timeline_fetch_failed": "Fehler beim Anfragen der Timeline" | ||||
|     }, | ||||
| 
 | ||||
|     "error": { | ||||
|         "bad_request": "Fehlgeschlagener Request", | ||||
|         "invalid_auth_code": "Ungültiger Authcode", | ||||
|         "connection_failed": "Verbindung fehlgeschlagen <code>%1</code>.", | ||||
|         "post_fetch_failed": "Fehler beim Anzeigen des Posts <code>%1</code>." | ||||
|     }, | ||||
| 
 | ||||
|     "time": { | ||||
|         "in": "In %1", | ||||
|         "ago": "Vor %1", | ||||
|         "second": "s", | ||||
|         "minute": "m", | ||||
|         "hour": "h", | ||||
|         "day": "t", | ||||
|         "week": "w", | ||||
|         "year": "j" | ||||
|     }, | ||||
| 
 | ||||
|     "compose": "Posten", | ||||
|     "search": "Suchen", | ||||
|     "loading": "Nur ein Moment...", | ||||
| 
 | ||||
|     "source": "Quellcode", | ||||
|     "issues": "Issues" | ||||
| } | ||||
							
								
								
									
										151
									
								
								src/lang/en_GB.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/lang/en_GB.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,151 @@ | |||
| { | ||||
|     "compose_placeholders": [ | ||||
|         "What's cooking, %1?", | ||||
|         "Speak your mind!", | ||||
|         "Federate something...", | ||||
|         "I sure love posting!", | ||||
|         "Another day, another %1 post!" | ||||
|     ], | ||||
| 
 | ||||
|     "login": { | ||||
|         "welcome": "Welcome, fediverse user!", | ||||
|         "enter_domain": "Please enter your server domain to log in.", | ||||
|         "experimental": "Please note this is\n<strong><em>extremely experimental software</em></strong>;\nthings are likely to break!\n<br>\nIf that's all cool with you, welcome aboard!", | ||||
|         "button": "Log in", | ||||
|         "error": { | ||||
|             "no_domain": "Please enter a server domain.", | ||||
|             "connection_failed": "Failed to connect to the server.\nCheck the browser console for details!", | ||||
|             "create_app": "Failed to create an application for this server." | ||||
|         }, | ||||
|         "made_with_tagline": "made with ❤ by <a href=\"https://bliss.town\">bliss town</a>" | ||||
|     }, | ||||
| 
 | ||||
|     "navigation": { | ||||
|         "timeline": "Timeline", | ||||
|         "notifications": "Notifications", | ||||
|         "follow_requests": "Follow requests", | ||||
|         "explore": "Explore", | ||||
|         "lists": "Lists", | ||||
| 
 | ||||
|         "favourites": "Favourites", | ||||
|         "bookmarks": "Bookmarks", | ||||
|         "hashtags": "Hashtags", | ||||
| 
 | ||||
|         "profile_information": "Profile information", | ||||
|         "settings": "Settings", | ||||
|         "log_out": "Log out", | ||||
| 
 | ||||
|         "back": "Back" | ||||
|     }, | ||||
| 
 | ||||
|     "follow_requests": { | ||||
|         "none": "no follow requests to action right now!" | ||||
|     }, | ||||
| 
 | ||||
|     "timeline": { | ||||
|         "home": "Home", | ||||
|         "local": "Local", | ||||
|         "federated": "Federated", | ||||
|         "fetching": "getting the feed..." | ||||
|     }, | ||||
| 
 | ||||
|     "notification": { | ||||
|         "and_others": "and <strong>%1</strong> others", | ||||
|         "mention": "%1 mentioned you.", | ||||
|         "reblog": "%1 boosted your post.", | ||||
|         "reaction": "%1 reacted to your post.", | ||||
|         "follow": "%1 followed you.", | ||||
|         "follow_request": "%1 requested to follow you.", | ||||
|         "favourite": "%1 favourited your post.", | ||||
|         "poll": "%1's poll has ended.", | ||||
|         "update": "%1 updated their post.", | ||||
|         "default": "%1 poked you!", | ||||
|         "fetching": "fetching notifications..." | ||||
|     }, | ||||
| 
 | ||||
|     "post": { | ||||
|         "loading": "loading post...", | ||||
|         "pinned": "📌 Pinned post", | ||||
|         "by": "Post by %1", | ||||
|         "boosted": "%1 boosted this post.", | ||||
|         "actions": { | ||||
|             "reply": "Reply", | ||||
|             "boost": "Boost", | ||||
|             "favourite": "Favourite", | ||||
|             "quote": "Quote", | ||||
|             "react": "React", | ||||
|             "more": "More", | ||||
|             "delete": "Delete" | ||||
|         }, | ||||
|         "warning": { | ||||
|             "placeholder": "Content warning", | ||||
|             "show": "(click to reveal)", | ||||
|             "hide": "(click to hide)" | ||||
|         }, | ||||
|         "visibility": { | ||||
|             "public": "public", | ||||
|             "unlisted": "unlisted", | ||||
|             "follow_only": "followers only", | ||||
|             "private": "private", | ||||
|             "direct": "direct" | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     "profile": { | ||||
|         "locked": "This is a private account.", | ||||
|         "bot": "This is an automated account.", | ||||
|         "followers": "Followers", | ||||
|         "following": "Following", | ||||
|         "follow": "Follow", | ||||
|         "home_instance": "View on home instance", | ||||
|         "more": "More", | ||||
|         "posts": "Posts", | ||||
|         "replies": "Replies", | ||||
|         "media": "Media", | ||||
|         "loading": "loading profile..." | ||||
|     }, | ||||
| 
 | ||||
|     "logs": { | ||||
|         "logged_in": "Logged in as %1", | ||||
|         "server_detected": "Server detected as %1 (%2) with capabilities: {%3}}", | ||||
|         "server_unsupported": "Server %1 is unsupported (%2). Things may break, or not work as expected", | ||||
|         "no_hostname": "Attempted to connect to a server without providing a hostname", | ||||
|         "no_https": "Cowardly refusing to connect to an insecure server", | ||||
|         "connection_failed": "Failed to connect to %1", | ||||
|         "post_fetch_failed": "Failed to fetch post", | ||||
|         "post_fetch_failed_id": "Failed to fetch post %1", | ||||
|         "post_parse_failed": "Failed to parse post", | ||||
|         "post_parse_failed_id": "Failed to parse post %1", | ||||
|         "profile_fetch_failed": "Failed to fetch profile", | ||||
|         "profile_fetch_failed_id": "Failed to fetch profile %1", | ||||
|         "token_revoke_failed": "Token revocation failed! Dumping data anyways", | ||||
|         "sound_does_not_exist": "Attempted to play sound \"%1\", which does not exist!", | ||||
|         "account_data_empty": "Attempted to parse account data but no data was provided", | ||||
|         "timeline_fetch_failed": "Failed to retrieve timeline." | ||||
|     }, | ||||
| 
 | ||||
|     "error": { | ||||
|         "bad_request": "Bad request", | ||||
|         "invalid_auth_code": "Invalid auth code provided", | ||||
|         "connection_failed": "Failed to connect to <code>%1</code>.", | ||||
|         "post_fetch_failed": "Failed to retrieve post <code>%1</code>." | ||||
|     }, | ||||
| 
 | ||||
|     "time": { | ||||
|         "in": "in %1", | ||||
|         "ago": "%1 ago", | ||||
|         "second": "s", | ||||
|         "minute": "m", | ||||
|         "hour": "h", | ||||
|         "day": "d", | ||||
|         "week": "w", | ||||
|         "year": "y" | ||||
|     }, | ||||
| 
 | ||||
|     "compose": "Post", | ||||
|     "search": "Search", | ||||
|     "loading": "just a moment...", | ||||
| 
 | ||||
|     "source": "source", | ||||
|     "issues": "issues" | ||||
| } | ||||
							
								
								
									
										145
									
								
								src/lang/ga_IE.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/lang/ga_IE.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,145 @@ | |||
| { | ||||
|     "compose_placeholders": [ | ||||
|         "Céard atá ar bun, %1?", | ||||
|         "Abair do thuairim!", | ||||
|         "Cónaidhmigh rud éigin...", | ||||
|         "Is breá liom postáil cinnte!", | ||||
|         "Lá eile, postáil %1 eile!" | ||||
|     ], | ||||
| 
 | ||||
|     "login": { | ||||
|         "welcome": "Fáilte, a úsáideoir fediverse!", | ||||
|         "enter_domain": "Cuir isteach fearann do fhreastalaí le logáil isteach.", | ||||
|         "experimental": "Tabhair faoi deara gur\n<strong><em>bogearraí thar a bheith turgnamhach</em> iad seo</strong>;\nis dócha go mbrisfidh rudaí!\n<br>\nMás ceart go leor leat é sin, fáilte romhat!", | ||||
|         "button": "Logáil isteach", | ||||
|         "error": { | ||||
|             "no_domain": "Cuir isteach fearann freastalaí.", | ||||
|             "connection_failed": "Theip ar cheangal leis an bhfreastalaí.\nSeiceáil consól an bhrabhsálaí le haghaidh sonraí!", | ||||
|             "create_app": "Theip ar fheidhmchlár a chruthú don fhreastalaí seo." | ||||
|         }, | ||||
|         "made_with_tagline": "déanta le ❤ ag <a href=\"https://bliss.town\">bliss town</a>" | ||||
|     }, | ||||
| 
 | ||||
|     "navigation": { | ||||
|         "timeline": "Amlíne", | ||||
|         "notifications": "Fógraí", | ||||
|         "explore": "Féach Thart", | ||||
|         "lists": "Liostaí", | ||||
| 
 | ||||
|         "favourites": "Ceanáin", | ||||
|         "bookmarks": "Leabharmharcanna", | ||||
|         "hashtags": "Haischlibeanna", | ||||
| 
 | ||||
|         "profile_information": "Faisnéis Phróifíle", | ||||
|         "settings": "Socruithe", | ||||
|         "log_out": "Logáil amach", | ||||
| 
 | ||||
|         "back": "Fill" | ||||
|     }, | ||||
| 
 | ||||
|     "timeline": { | ||||
|         "home": "Baile", | ||||
|         "local": "Áitiúil", | ||||
|         "federated": "Cónaidhmithe", | ||||
|         "fetching": "ag fáil an fhotha..." | ||||
|     }, | ||||
| 
 | ||||
|     "notification": { | ||||
|         "and_others": "agus <strong>%1</strong> eile", | ||||
|         "mention": "Luaigh %1 thú.", | ||||
|         "reblog": "Chuir %1 borradh faoi do phost", | ||||
|         "reaction": "D'fhrithghníomhaigh %1 do do phost", | ||||
|         "follow": "Lean %1 thú", | ||||
|         "follow_request": "D'iarr %1 leanúint leat.", | ||||
|         "favourite": "Chuir %1 do phost i bhfabhar.", | ||||
|         "poll": "Chríochnaigh pobalbhreith %1.", | ||||
|         "update": "Nuashonraigh %1 a bpost", | ||||
|         "default": "Priocadh %1 thú!", | ||||
|         "fetching": "ag fáil an fógraí..." | ||||
|     }, | ||||
| 
 | ||||
|     "post": { | ||||
|         "loading": "ag lódáil an postáil...", | ||||
|         "by": "Postáil le %1", | ||||
|         "boosted": "Chuir %1 borradh faoin bpost seo.", | ||||
|         "actions": { | ||||
|             "reply": "Freagair", | ||||
|             "boost": "Borradh", | ||||
|             "favourite": "Ceanán", | ||||
|             "quote": "Sliocht", | ||||
|             "react": "Frithghníomhaigh", | ||||
|             "more": "Tuilleadh", | ||||
|             "delete": "Scrios" | ||||
|         }, | ||||
|         "warning": { | ||||
|             "placeholder": "Rabhadh ábhair", | ||||
|             "show": "(cliceáil chun nochtadh)", | ||||
|             "hide": "(cliceáil chun a cheilt)" | ||||
|         }, | ||||
|         "visibility": { | ||||
|             "public": "poiblí", | ||||
|             "unlisted": "neamhliostaithe", | ||||
|             "follow_only": "leantóirí amháin", | ||||
|             "private": "príobháideach", | ||||
|             "direct": "díreach" | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     "profile": { | ||||
|         "locked": "Is cuntas príobháideach é seo.", | ||||
|         "bot": "Is cuntas uathoibrithe é seo.", | ||||
|         "followers": "Leantóirí", | ||||
|         "following": "Ag leanúint", | ||||
|         "follow": "Lean", | ||||
|         "home_instance": "Féach ar ásc baile", | ||||
|         "more": "Tuilleadh", | ||||
|         "posts": "Poist", | ||||
|         "replies": "Freagraí", | ||||
|         "media": "Meáin", | ||||
|         "loading": "ag lódáil an próifíl..." | ||||
|     }, | ||||
| 
 | ||||
|     "logs": { | ||||
|         "logged_in": "Logáilte isteach mar %1", | ||||
|         "server_detected": "Freastalaí braite mar %1 (%2) le cumais: {%3}}", | ||||
|         "server_unsupported": "Ní thacaítear le freastalaí %1 (%2). D'fhéadfadh rudaí briseadh, nó gan oibriú mar a bhíothas ag súil leis.", | ||||
|         "no_hostname": "Rinneadh iarracht ceangal le freastalaí gan ainm óstach a sholáthar", | ||||
|         "no_https": "Diúltaíodh ceangal leis an bhfreastalaí neamhshábháilte cosúil le cladhaire", | ||||
|         "connection_failed": "Theip ar cheangal le %1", | ||||
|         "post_fetch_failed": "Theip ar an bpost a fháil", | ||||
|         "post_fetch_failed_id": "Theip ar an bpost %1 a fháil", | ||||
|         "post_parse_failed": "Theip ar an bpost a pharsáil", | ||||
|         "post_parse_failed_id": "Theip ar an bpost %1 a pharsáil", | ||||
|         "profile_fetch_failed": "Theip ar phróifíl a fháil", | ||||
|         "profile_fetch_failed_id": "Theip ar phróifíl a fháil %1", | ||||
|         "token_revoke_failed": "Theip ar chúlghairm an chomhartha! Ag dumpáil sonraí ar aon nós", | ||||
|         "sound_does_not_exist": "Rinneadh iarracht fuaim \"%1\" a sheinm, ach níl sé ann!", | ||||
|         "account_data_empty": "Rinneadh iarracht sonraí cuntais a pharsáil ach níor soláthraíodh aon sonraí", | ||||
|         "timeline_fetch_failed": "Theip ar an amlíne a aisghabháil." | ||||
|     }, | ||||
| 
 | ||||
|     "error": { | ||||
|         "bad_request": "Droch-iarratas", | ||||
|         "invalid_auth_code": "Cód údaraithe neamhbhailí curtha ar fáil", | ||||
|         "connection_failed": "Theip ar cheangal le <code>%1</code>.", | ||||
|         "post_fetch_failed": "Theip ar aisghabháil an phoist <code>%1</code>." | ||||
|     }, | ||||
| 
 | ||||
|     "time": { | ||||
|         "in": "i gceann %1", | ||||
|         "ago": "%1 ó shin", | ||||
|         "second": "so", | ||||
|         "minute": "no", | ||||
|         "hour": "ua", | ||||
|         "day": "lá", | ||||
|         "week": "se", | ||||
|         "year": "bl" | ||||
|     }, | ||||
| 
 | ||||
|     "compose": "Cruthaigh", | ||||
|     "search": "Cuardaigh", | ||||
|     "loading": "fan nóiméad...", | ||||
| 
 | ||||
|     "source": "foinse", | ||||
|     "issues": "saincheisteanna" | ||||
| } | ||||
|  | @ -1,6 +1,9 @@ | |||
| import { server } from '$lib/client/server.js'; | ||||
| import { parseEmoji, renderEmoji } from '$lib/emoji.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| import Lang from '$lib/lang'; | ||||
| 
 | ||||
| const lang = Lang(); | ||||
| 
 | ||||
| const cache = writable({}); | ||||
| 
 | ||||
|  | @ -11,7 +14,7 @@ const cache = writable({}); | |||
|  */ | ||||
| export function parseAccount(data) { | ||||
|     if (!data) { | ||||
|         console.error("Attempted to parse account data but no data was provided"); | ||||
|         console.error(lang.string('logs.account_data_empty')); | ||||
|         return null; | ||||
|     } | ||||
|     let account = get(cache)[data.id]; | ||||
|  | @ -20,17 +23,26 @@ export function parseAccount(data) { | |||
| 
 | ||||
|     account = {}; | ||||
|     account.id = data.id; | ||||
|     account.nickname = data.display_name.trim(); | ||||
|     account.nickname = data.display_name.trim().replaceAll('<', '<').replaceAll('>', '>'); | ||||
|     account.username = data.username; | ||||
|     account.name = account.nickname || account.username; | ||||
|     account.avatar_url = data.avatar; | ||||
|     account.banner_url = data.header; | ||||
|     account.url = data.url; | ||||
|     account.followers_count = data.followers_count; | ||||
|     account.following_count = data.following_count; | ||||
|     account.posts_count = data.statuses_count; | ||||
|     account.bio = data.note; | ||||
|     account.bot = data.bot; | ||||
|     account.locked = data.locked; | ||||
| 
 | ||||
|     if (data.acct.includes('@')) | ||||
|         account.host = data.acct.split('@')[1]; | ||||
|     else | ||||
|         account.host = get(server).host; | ||||
| 
 | ||||
|     account.fqn = data.fqn || account.username + "@" + account.host; | ||||
| 
 | ||||
|     account.mention = "@" + account.username; | ||||
|     if (account.host != get(server).host) | ||||
|         account.mention += "@" + account.host; | ||||
|  | @ -41,6 +53,7 @@ export function parseAccount(data) { | |||
|     }); | ||||
| 
 | ||||
|     account.rich_name = account.nickname ? renderEmoji(account.nickname, account.emojis) : account.username; | ||||
|     account.rich_bio = renderEmoji(account.bio, account.emojis); | ||||
| 
 | ||||
|     cache.update(cache => { | ||||
|         cache[account.id] = account; | ||||
|  |  | |||
							
								
								
									
										212
									
								
								src/lib/api.js
									
										
									
									
									
								
							
							
						
						
									
										212
									
								
								src/lib/api.js
									
										
									
									
									
								
							|  | @ -1,3 +1,32 @@ | |||
| const errors = { | ||||
|     AUTHENTICATION_FAILED: "AUTHENTICATION_FAILED", | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Parses a HTTP Link header | ||||
|  * @param {string} header - the HTTP Link header string | ||||
|  */ | ||||
| function _parseLinkHeader(header) { | ||||
|     // remove whitespace and split
 | ||||
|     let links = header.replace(/\ /g, "").split(","); | ||||
| 
 | ||||
|     return links.map(l => { | ||||
|         let parts = l.split(";"); | ||||
| 
 | ||||
|         // assuming 0th is URL, removing <>
 | ||||
|         let url = new URL(parts[0].slice(1, -1)); | ||||
| 
 | ||||
|         // get rel inbetween double-quotes
 | ||||
|         let rel = parts[1].match(/"(.*?)"/g)[0].slice(1, -1); | ||||
| 
 | ||||
|         return { | ||||
|             url, rel | ||||
|         } | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| _parseLinkHeader(`<https://wetdry.world/api/v1/timelines/home?max_id=114857293229157171>; rel="next", <https://wetdry.world/api/v1/timelines/home?min_id=114857736990577458>; rel="prev"`) | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/instance | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  | @ -172,16 +201,112 @@ export async function getNotifications(host, token, min_id, max_id, limit, types | |||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/follow_requests | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} min_id - If provided, only shows follow requests since this ID. | ||||
|  * @param {string} max_id - If provided, only shows follow requests before this ID. | ||||
|  * @param {string} limit - The maximum number of follow requests to retrieve (default 40, max 80). | ||||
|  */ | ||||
| export async function getFollowRequests(host, token, since_id, max_id, limit) { | ||||
|     let url = `https://${host}/api/v1/follow_requests`; | ||||
| 
 | ||||
|     let params = new URLSearchParams(); | ||||
|     if (since_id) params.append("since_id", since_id); | ||||
|     if (max_id) params.append("max_id", max_id); | ||||
|     if (limit) params.append("limit", limit); | ||||
|     const params_string = params.toString(); | ||||
|     if (params_string) url += '?' + params_string; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/follow_requests/:account_id/authorize | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} account_id - The account ID of the follow request to accept | ||||
|  */ | ||||
| export async function acceptFollowRequest(host, token, account_id) { | ||||
|     let url = `https://${host}/api/v1/follow_requests/${account_id}/authorize`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": "Bearer " + token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/follow_requests/:account_id/reject | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} account_id - The account ID of the follow request to reject | ||||
|  */ | ||||
| export async function rejectFollowRequest(host, token, account_id) { | ||||
|     let url = `https://${host}/api/v1/follow_requests/${account_id}/reject`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": "Bearer " + token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/timelines/{timeline} | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} timeline - The name of the timeline to pull (default "home"). | ||||
|  * @param {string} max_id - If provided, only shows posts after this ID. | ||||
|  * @param {boolean} local_only - If provided, only shows posts from the local instance | ||||
|  * @param {boolean} remote_only - If provided, only shows posts from other instances | ||||
|  */ | ||||
| export async function getTimeline(host, token, timeline, max_id) { | ||||
| export async function getTimeline(host, token, timeline, max_id, local_only, remote_only) { | ||||
|     let url = `https://${host}/api/v1/timelines/${timeline || "home"}`; | ||||
| 
 | ||||
|     let params = new URLSearchParams(); | ||||
|     if (max_id) params.append("max_id", max_id); | ||||
|     if (remote_only) params.append("remote", remote_only); | ||||
|     if (local_only) params.append("local", local_only); | ||||
|     const params_string = params.toString(); | ||||
|     if (params_string) url += '?' + params_string; | ||||
|      | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }) | ||||
| 
 | ||||
|     let res = { | ||||
|         data: await data.json() | ||||
|     } | ||||
| 
 | ||||
|     if(data.headers.has("Link")) { | ||||
|         let links = _parseLinkHeader(data.headers.get("Link")); | ||||
|         res["prev"] = links.find(f=>f.rel=="prev"); | ||||
|         res["next"] = links.find(f=>f.rel=="next"); | ||||
|     } | ||||
| 
 | ||||
|     return res; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/favourites | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} max_id - If provided, only shows posts after this ID. | ||||
|  */ | ||||
| export async function getFavourites(host, token, max_id) { | ||||
|     let url = `https://${host}/api/v1/favourites`; | ||||
| 
 | ||||
|     let params = new URLSearchParams(); | ||||
|     if (max_id) params.append("max_id", max_id); | ||||
|     const params_string = params.toString(); | ||||
|  | @ -190,9 +315,19 @@ export async function getTimeline(host, token, timeline, max_id) { | |||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }).then(res => res.json()); | ||||
|     }) | ||||
|      | ||||
|     return data; | ||||
|     let res = { | ||||
|         data: await data.json() | ||||
|     } | ||||
| 
 | ||||
|     if(data.headers.has("Link")) { | ||||
|         let links = _parseLinkHeader(data.headers.get("Link")); | ||||
|         res["prev"] = links.find(f=>f.rel=="prev"); | ||||
|         res["next"] = links.find(f=>f.rel=="next"); | ||||
|     } | ||||
| 
 | ||||
|     return res; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -421,3 +556,74 @@ export async function getUser(host, token, user_id) { | |||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/accounts/lookup?acct={handle} | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} handle - The handle of the user to fetch. | ||||
|  */ | ||||
| export async function lookupUser(host, token, handle) { | ||||
|     let url = `https://${host}/api/v1/accounts/lookup?acct=${handle}`; | ||||
| 
 | ||||
|     const res = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }); | ||||
|     if (!res.ok) { | ||||
|         const json = await res.json(); | ||||
|         if (json.error = errors.AUTHENTICATION_FAILED) | ||||
|             throw new Error("This method requires authentication"); | ||||
|     } | ||||
|     const data = await res.json(); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/accounts/{user_id}/statuses | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} user_id - The ID of the user to fetch. | ||||
|  * @param {string} max_id - If provided, only shows notifications before this ID. | ||||
|  * @param {boolean} replies - If replies should be fetched. | ||||
|  * @param {boolean} boosts - If boosts should be fetched. | ||||
|  * @param {boolean} only_media - If only media should be fetched. | ||||
|  */ | ||||
| export async function getUserPosts(host, token, user_id, max_id, show_replies, show_boosts, only_media) { | ||||
|     let url = new URL(`https://${host}/api/v1/accounts/${user_id}/statuses`); | ||||
|     let query = []; | ||||
|     if (!show_replies) | ||||
|         query.push('exclude_replies=true'); | ||||
|     if (!show_boosts) | ||||
|         query.push('exclude_boosts=true'); | ||||
|     if (only_media) | ||||
|         query.push('only_media=true'); | ||||
|     if (max_id) | ||||
|         query.push(`max_id=${max_id}`); | ||||
|     url.search = query.join('&'); | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/accounts/{user_id}/statuses?pinned=true | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} user_id - The ID of the user to fetch. | ||||
|  */ | ||||
| export async function getUserPinnedPosts(host, token, user_id) { | ||||
|     let url = `https://${host}/api/v1/accounts/${user_id}/statuses?pinned=true`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
|  | @ -2,6 +2,9 @@ import * as api from '$lib/api.js'; | |||
| import { writable } from 'svelte/store'; | ||||
| import { app_name } from '$lib/config.js'; | ||||
| import { browser } from "$app/environment"; | ||||
| import Lang from '$lib/lang'; | ||||
| 
 | ||||
| const lang = Lang(); | ||||
| 
 | ||||
| const server_types = { | ||||
|     UNSUPPORTED: "unsupported", | ||||
|  | @ -35,11 +38,11 @@ server.subscribe(server => { | |||
|  */ | ||||
| export async function createServer(host) { | ||||
|     if (!host) { | ||||
|         console.error("Attempted to create server without providing a hostname"); | ||||
|         console.error(lang.string('logs.no_hostname')); | ||||
|         return false; | ||||
|     } | ||||
|     if (host.startsWith("http://")) { | ||||
|         console.error("Cowardly refusing to connect to an insecure server"); | ||||
|         console.error(lang.string('logs.no_https')); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|  | @ -49,7 +52,7 @@ export async function createServer(host) { | |||
|     if (host.startsWith("https://")) host = host.substring(8); | ||||
|     const data = await api.getInstance(host); | ||||
|     if (!data) { | ||||
|         console.error(`Failed to connect to ${host}`); | ||||
|         console.error(lang.string('logs.connection_failed', host)); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|  | @ -58,9 +61,9 @@ export async function createServer(host) { | |||
|     server.capabilities = getCapabilities(server.type); | ||||
| 
 | ||||
|     if (server.type === server_types.UNSUPPORTED) { | ||||
|         console.warn(`Server ${host} is unsupported (${server.version}). Things may break, or not work as expected`); | ||||
|         console.warn(lang.string('logs.server_unsupported', host, server.version)); | ||||
|     } else { | ||||
|         console.log(`Server detected as "${server.type}" (${server.version}) with capabilities: {${server.capabilities.join(', ')}}`); | ||||
|         console.log(lang.string('logs.server_detected', server.type, server.version, server.capabilities.join(', '))); | ||||
|     } | ||||
| 
 | ||||
|     return server; | ||||
|  |  | |||
							
								
								
									
										28
									
								
								src/lib/followRequests.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/lib/followRequests.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| import { server } from './client/server.js'; | ||||
| import { writable } from "svelte/store"; | ||||
| import * as api from "./api.js"; | ||||
| import { app } from './client/app.js'; | ||||
| import { get } from 'svelte/store'; | ||||
| import { parseAccount } from './account.js'; | ||||
| 
 | ||||
| // Cache for all requests
 | ||||
| export let followRequests = writable(); | ||||
| 
 | ||||
| /** | ||||
|  * Gets all follow requests | ||||
|  * @param {boolean} force | ||||
|  */ | ||||
| export async function fetchFollowRequests(force) { | ||||
|     // if already cached, return for now
 | ||||
|     if(!get(followRequests) && !force) return; | ||||
| 
 | ||||
|     let newReqs = await api.getFollowRequests( | ||||
|         get(server).host, | ||||
|         get(app).token | ||||
|     ); | ||||
| 
 | ||||
|     // parse accounts
 | ||||
|     newReqs = newReqs.map((r) => parseAccount(r)); | ||||
| 
 | ||||
|     followRequests.set(newReqs); | ||||
| } | ||||
							
								
								
									
										65
									
								
								src/lib/lang.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/lib/lang.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| import * as en_GB from '@cf/lang/en_GB.json'; | ||||
| // import * as ga_IE from '@cf/lang/ga_IE.json';
 | ||||
| // import * as de_DE from '@cf/lang/de_DE.json';
 | ||||
| 
 | ||||
| /** | ||||
|  * @returns Map<string, string | string[]> | ||||
|  */ | ||||
| export default function init() { | ||||
|     let i18n = new Object(); | ||||
| 
 | ||||
|     // TODO: dynamic loading of language files
 | ||||
|     let language = en_GB; | ||||
|     let lang_code = 'en_GB'; | ||||
| 
 | ||||
|     i18n.lang = language; | ||||
|     i18n.lang_code = lang_code; | ||||
|     i18n.string = function(/* @type string */ key, ...args) { | ||||
|         const tokens = key.split('.'); | ||||
|          | ||||
|         let i = 0; | ||||
|         let token = tokens[i]; | ||||
|         let res = this.lang; | ||||
|         while (true) { | ||||
|             res = res[token]; | ||||
|             if (res === undefined) { | ||||
|                 console.warn(`${key} not found for language ${this.lang_code}`); | ||||
|                 return key; | ||||
|             } | ||||
|             if (typeof res === 'string' || res instanceof String) | ||||
|                 break; | ||||
|             i++; | ||||
|             token = tokens[i]; | ||||
|         } | ||||
|          | ||||
|         i = 1; | ||||
|         while (true) { | ||||
|             if (args.length < i || !res.includes('%' + i)) | ||||
|                 break; | ||||
|             res = res.replaceAll('%' + i, args[i - 1]); | ||||
|             i++; | ||||
|         } | ||||
| 
 | ||||
|         return res; | ||||
|     } | ||||
|     i18n.stringArray = function(/* @type string */ key) { | ||||
|         const tokens = key.split('.'); | ||||
|          | ||||
|         let i = 0; | ||||
|         let token = tokens[i]; | ||||
|         let res = this.lang; | ||||
|         while (true) { | ||||
|             res = res[token]; | ||||
|             if (res === undefined) { | ||||
|                 console.warn(`${key} not found for language ${this.lang_code}`); | ||||
|                 return key; | ||||
|             } | ||||
|             if (Array.isArray(res)) | ||||
|                 return res; | ||||
|             i++; | ||||
|             token = tokens[i]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return i18n; | ||||
| } | ||||
|  | @ -1,3 +1,7 @@ | |||
| import Lang from '$lib/lang'; | ||||
| 
 | ||||
| const lang = Lang(); | ||||
| 
 | ||||
| import sound_log from '../sound/log.ogg'; | ||||
| import sound_hello from '../sound/hello.ogg'; | ||||
| import sound_success from '../sound/success.ogg'; | ||||
|  | @ -16,7 +20,7 @@ export function playSound(name) { | |||
|     if (!name) name = "default"; | ||||
|     const sound = sounds[name]; | ||||
|     if (!sound) { | ||||
|         console.warn(`Attempted to play sound "${name}", which does not exist!`); | ||||
|         console.warn(lang.string('lang.sound_does_not_exist', name)); | ||||
|         return; | ||||
|     } | ||||
|     sound.pause(); | ||||
|  |  | |||
|  | @ -1,10 +1,13 @@ | |||
| import Lang from '$lib/lang'; | ||||
| const lang = Lang(); | ||||
| 
 | ||||
| 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 }, | ||||
|     { unit: lang.string('time.second'), min: 0 }, | ||||
|     { unit: lang.string('time.minute'), min: 60 }, | ||||
|     { unit: lang.string('time.hour'), min: 60 }, | ||||
|     { unit: lang.string('time.day'), min: 24 }, | ||||
|     { unit: lang.string('time.week'), min: 7 }, | ||||
|     { unit: lang.string('time.year'), min: 52 }, | ||||
| ]; | ||||
| 
 | ||||
| export function shorthand(date) { | ||||
|  | @ -18,6 +21,6 @@ export function shorthand(date) { | |||
|         unit = denoms[index].unit; | ||||
|     } | ||||
|     if (value > 0) | ||||
|         return Math.floor(value) + unit + " ago"; | ||||
|     return "in " + Math.floor(value) + unit; | ||||
|         return lang.string('time.ago').replaceAll('%1', Math.floor(value) + unit); | ||||
|     return lang.string('time.in').replaceAll('%1', Math.floor(value) + unit); | ||||
| } | ||||
|  |  | |||
|  | @ -3,44 +3,72 @@ import { server } from '$lib/client/server.js'; | |||
| import { app } from '$lib/client/app.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| import { parsePost } from '$lib/post.js'; | ||||
| import Lang from '$lib/lang'; | ||||
| 
 | ||||
| export const timeline = writable([]); | ||||
| 
 | ||||
| let loading = false; | ||||
| const lang = Lang(); | ||||
| 
 | ||||
| export async function getTimeline(clean) { | ||||
| let loading = false; | ||||
| let last_post = false;      // last post marker, used for fetching next sequence of posts
 | ||||
| let at_end = false;         // at end of timeline, no next param to paginate
 | ||||
| 
 | ||||
| export async function getTimeline(timelineType = "home", clean, localOnly = false, remoteOnly = false) { | ||||
|     if (loading) return; // no spamming!!
 | ||||
|     loading = true; | ||||
| 
 | ||||
|     let last_post = false; | ||||
|     if (!clean && get(timeline).length > 0) | ||||
|         last_post = get(timeline)[get(timeline).length - 1].id; | ||||
|     if(at_end) return; | ||||
| 
 | ||||
|     const timeline_data = await api.getTimeline( | ||||
|     if(clean) { | ||||
|         timeline.set([]); | ||||
|         last_post = false; | ||||
|     } | ||||
| 
 | ||||
|     let timeline_data; | ||||
|     switch(timelineType) { | ||||
|         case "favourites": | ||||
|             timeline_data = await api.getFavourites( | ||||
|                 get(server).host, | ||||
|                 get(app).token, | ||||
|         "home", | ||||
|                 last_post | ||||
|             ) | ||||
|             break; | ||||
|              | ||||
|         default: | ||||
|             timeline_data = await api.getTimeline( | ||||
|                 get(server).host, | ||||
|                 get(app).token, | ||||
|                 timelineType, | ||||
|                 last_post, | ||||
|                 localOnly, | ||||
|                 remoteOnly | ||||
|             ); | ||||
|             break; | ||||
|     } | ||||
| 
 | ||||
|     if (!timeline_data) { | ||||
|         console.error(`Failed to retrieve timeline.`); | ||||
|         console.error(lang.string('logs.timeline_fetch_failed')); | ||||
|         loading = false; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (clean) timeline.set([]); | ||||
|     if (!clean && timeline_data.next) { | ||||
|         last_post = timeline_data.next.url.searchParams.get("max_id") | ||||
|     } else if(!timeline_data.next) { | ||||
|         console.log(timeline_data) | ||||
|         at_end = true; | ||||
|     } | ||||
| 
 | ||||
|     for (let i in timeline_data) { | ||||
|         const post_data = timeline_data[i]; | ||||
|     for (let i in timeline_data.data) { | ||||
|         const post_data = timeline_data.data[i]; | ||||
|         const post = await parsePost(post_data, 1); | ||||
|         if (!post) { | ||||
|             if (post === null || post === undefined) { | ||||
|                 if (post_data.id) { | ||||
|                     console.warn("Failed to parse post #" + post_data.id); | ||||
|                     console.warn(lang.string('logs.post_parse_failed_id', post_data.id)); | ||||
|                 } else { | ||||
|                     console.warn("Failed to parse post:"); | ||||
|                     console.warn(post_data); | ||||
|                     console.warn(lang.string('logs.post_parse_failed')); | ||||
|                     console.debug(post_data); | ||||
|                 } | ||||
|             } | ||||
|             continue; | ||||
|  | @ -49,3 +77,9 @@ export async function getTimeline(clean) { | |||
|     } | ||||
|     loading = false; | ||||
| } | ||||
| 
 | ||||
| export function clearTimeline() { | ||||
|     timeline.set([]); | ||||
|     last_post = false; | ||||
|     at_end = false; | ||||
| } | ||||
|  | @ -5,35 +5,47 @@ | |||
| 
 | ||||
|     const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
|     let className = ""; | ||||
|     export { className as class }; | ||||
|     export let active = false; | ||||
|     export let filled = false; | ||||
|     export let disabled = false; | ||||
|     export let centered = false; | ||||
|     export let label = undefined; | ||||
|     export let sound = "default"; | ||||
|     export let href = false; | ||||
|     export let href = undefined; | ||||
|     export let onClick = undefined; | ||||
| 
 | ||||
|     let classes = []; | ||||
| 
 | ||||
|     function click() { | ||||
|         if (disabled) return; | ||||
|         if (href) { | ||||
|             location = href; | ||||
|             return; | ||||
|         } | ||||
|         playSound(sound); | ||||
|         dispatch('click'); | ||||
|     } | ||||
| 
 | ||||
|     afterUpdate(() => { | ||||
|         classes = []; | ||||
|         if (active) classes = ["active"]; | ||||
|         if (filled) classes = ["filled"]; | ||||
|         if (disabled) classes = ["disabled"]; | ||||
|         classes = className.split(' '); | ||||
|         if (active) classes.push("active"); | ||||
|         if (filled) classes.push("filled"); | ||||
|         if (disabled) classes.push("disabled"); | ||||
|         if (centered) classes.push("centered"); | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| {#if href} | ||||
|     <a | ||||
|             class={classes.join(' ')} | ||||
|             title={label} | ||||
|             aria-label={label} | ||||
|             href={href} | ||||
|             on:click={() => click()}> | ||||
|         <span class="icon"> | ||||
|             <slot name="icon" /> | ||||
|         </span> | ||||
|         <slot/> | ||||
|     </a> | ||||
| {:else} | ||||
|     <button | ||||
|             type="button" | ||||
|             class={classes.join(' ')} | ||||
|  | @ -45,13 +57,12 @@ | |||
|             </span> | ||||
|             <slot/> | ||||
|     </button> | ||||
| {/if} | ||||
| 
 | ||||
| <style> | ||||
|     button { | ||||
|         /* min-width: 64px; */ | ||||
|         width: 100%; | ||||
|         height: 54px; | ||||
|         padding: 16px; | ||||
|     a, button { | ||||
|         height: fit-content; | ||||
|         padding: .7em .8em; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|  | @ -60,14 +71,13 @@ | |||
|         font-size: 1rem; | ||||
|         font-weight: 600; | ||||
|         text-align: left; | ||||
|         text-decoration: none; | ||||
| 
 | ||||
|         border-radius: 8px; | ||||
|         border-width: 2px; | ||||
|         border-style: solid; | ||||
|         border: 2px solid var(--bg-700); | ||||
| 
 | ||||
|         background-color: var(--bg-700); | ||||
|         color: var(--text); | ||||
|         border-color: transparent; | ||||
| 
 | ||||
|         transition-property: border-color, background-color, color; | ||||
|         transition-timing-function: ease-out; | ||||
|  | @ -75,22 +85,32 @@ | |||
| 
 | ||||
|         cursor: pointer; | ||||
|     } | ||||
|     a { | ||||
|         width: calc(100% - 1.6em); | ||||
|     } | ||||
|     button { | ||||
|         width: 100%; | ||||
|     } | ||||
| 
 | ||||
|     a.centered, | ||||
|     button.centered { | ||||
|         text-align: center; | ||||
|         justify-content: center; | ||||
|     } | ||||
| 
 | ||||
|     a:hover, | ||||
|     button:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--accent) 10%); | ||||
|         border-color: color-mix(in srgb, var(--bg-700), var(--accent) 20%); | ||||
|         border-color: color-mix(in srgb, var(--bg-700), black 10%); | ||||
|         background-color: color-mix(in srgb, var(--bg-700), black 10%); | ||||
|     } | ||||
| 
 | ||||
|     a:active, | ||||
|     button:active { | ||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 50%); | ||||
|         border-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 10%); | ||||
|         border-color: color-mix(in srgb, var(--bg-700), black 20%); | ||||
|         background-color: color-mix(in srgb, var(--bg-700), black 20%); | ||||
|     } | ||||
| 
 | ||||
|     a.active, | ||||
|     button.active { | ||||
|         background-color: var(--bg-600); | ||||
|         color: var(--accent); | ||||
|  | @ -98,50 +118,49 @@ | |||
|         text-shadow: 0px 2px 32px var(--accent); | ||||
|     } | ||||
| 
 | ||||
|     a.active:hover, | ||||
|     button.active:hover { | ||||
|         color: color-mix(in srgb, var(--accent), var(--bg-1000) 20%); | ||||
|         border-color: color-mix(in srgb, var(--accent), var(--bg-1000) 20%); | ||||
|         background-color: color-mix(in srgb, var(--bg-600), var(--accent) 10%); | ||||
|     } | ||||
| 
 | ||||
|     a.active:active, | ||||
|     button.active:active { | ||||
|         color: color-mix(in srgb, var(--accent), var(--bg-800) 10%); | ||||
|         border-color: color-mix(in srgb, var(--accent), var(--bg-800) 10%); | ||||
|         background-color: color-mix(in srgb, var(--bg-600), var(--bg-800) 10%); | ||||
|     } | ||||
| 
 | ||||
|     a.filled, | ||||
|     button.filled { | ||||
|         background-color: var(--accent); | ||||
|         color: var(--bg-800); | ||||
|         border-color: transparent; | ||||
|     } | ||||
| 
 | ||||
|     button.filled:hover { | ||||
|     a.filled:not(.disabled):hover, | ||||
|     button.filled:not(.disabled):hover { | ||||
|         color: color-mix(in srgb, var(--bg-800), white 10%); | ||||
|         background-color: color-mix(in srgb, var(--accent), white 20%); | ||||
|     } | ||||
| 
 | ||||
|     button.filled:active { | ||||
|     a.filled:not(.disabled):active, | ||||
|     button.filled:not(.disabled):active { | ||||
|         color: color-mix(in srgb, var(--bg-800), black 10%); | ||||
|         background-color: color-mix(in srgb, var(--accent), black 20%); | ||||
|     } | ||||
| 
 | ||||
|     a.disabled, | ||||
|     button.disabled { | ||||
|         background-color: var(--bg-700); | ||||
|         color: var(--text); | ||||
|         opacity: .5; | ||||
|         opacity: .35; | ||||
|         border-color: transparent; | ||||
|         cursor: initial; | ||||
|     } | ||||
| 
 | ||||
|     button.disabled:hover { | ||||
|     } | ||||
| 
 | ||||
|     button.disabled:active { | ||||
|         cursor: not-allowed; | ||||
|     } | ||||
| 
 | ||||
|     .icon:not(:empty) { | ||||
|         height: 150%; | ||||
|         height: 1.8em; | ||||
|         margin-right: 8px; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
|     import { timeline } from '$lib/timeline.js'; | ||||
|     import { createEventDispatcher } from 'svelte'; | ||||
|     import { playSound } from '$lib/sound'; | ||||
|     import Lang from '$lib/lang' | ||||
| 
 | ||||
|     import Button from '@cf/ui/Button.svelte'; | ||||
|     import PostIcon from '@cf/icons/post.svg'; | ||||
|  | @ -19,6 +20,8 @@ | |||
|     import FollowersVisIcon from '@cf/icons/followers.svg'; | ||||
|     import PrivateVisIcon from '@cf/icons/dm.svg'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     export let reply_id; | ||||
| 
 | ||||
|     let content_warning = "" | ||||
|  | @ -26,16 +29,11 @@ | |||
|     // let media_ids = []; | ||||
|     let show_cw = false; | ||||
|     let visibility = "Public"; | ||||
|     let visibilityLocale = lang.string('post.visibility.public'); | ||||
| 
 | ||||
|     const placeholders = [ | ||||
|         "What's cooking, $1?", | ||||
|         "Speak your mind!", | ||||
|         "Federate something...", | ||||
|         "I sure love posting!", | ||||
|         "Another day, another $1 post!", | ||||
|     ]; | ||||
|     let placeholder = placeholders[Math.floor(placeholders.length * Math.random())] | ||||
|         .replaceAll("$1", $account.username); | ||||
|     const placeholders = lang.stringArray('compose_placeholders'); | ||||
|     let placeholder = Array.isArray(placeholders) ? placeholders[Math.floor(placeholders.length * Math.random())] | ||||
|         .replaceAll("%1", $account.username) : placeholders; | ||||
| 
 | ||||
|     const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
|  | @ -77,15 +75,19 @@ | |||
|         switch (visibility) { | ||||
|             case "Public": | ||||
|                 visibility = "Unlisted"; | ||||
|                 visibilityLocale = lang.string('post.visibility.unlisted'); | ||||
|                 break; | ||||
|             case "Unlisted": | ||||
|                 visibility = "Followers only"; | ||||
|                 visibilityLocale = lang.string('post.visibility.follow_only'); | ||||
|                 break; | ||||
|             case "Followers only": | ||||
|                 visibility = "Private"; | ||||
|                 visibilityLocale = lang.string('post.visibility.private'); | ||||
|                 break; | ||||
|             case "Private": | ||||
|                 visibility = "Public"; | ||||
|                 visibilityLocale = lang.string('post.visibility.public'); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | @ -93,7 +95,8 @@ | |||
| 
 | ||||
| <div class="composer"> | ||||
|     <div class="composer-header-container"> | ||||
|         <a href={$account.url} target="_blank" class="composer-avatar-container" on:mouseup|stopPropagation> | ||||
|         <!-- TODO: account switcher in composer --> | ||||
|         <a href="" class="composer-avatar-container" on:mouseup|stopPropagation> | ||||
|             <img src={$account.avatar_url} type={$account.avatar_type} alt="" width="48" height="48" class="composer-avatar" loading="lazy" decoding="async"> | ||||
|         </a> | ||||
|         <header class="composer-header"> | ||||
|  | @ -104,7 +107,7 @@ | |||
|             <div class="composer-info" on:mouseup|stopPropagation> | ||||
|             </div> | ||||
|         </header> | ||||
|         <div title={visibility}> | ||||
|         <div title={visibilityLocale}> | ||||
|             <Button centered={true} on:click={() => {cycleVisibility()}}> | ||||
|                 <svelte:fragment slot="icon"> | ||||
|                     <!-- TODO: this should be a drop-down option!...later --> | ||||
|  | @ -122,7 +125,7 @@ | |||
|         </div> | ||||
|     </div> | ||||
|     {#if show_cw} | ||||
|         <input type="text" id="" placeholder="Content warning" bind:value={content_warning}/> | ||||
|         <input type="text" id="" placeholder="{lang.string('post.warning.placeholder')}" bind:value={content_warning}/> | ||||
|     {/if} | ||||
|     <textarea placeholder="{placeholder}" class="textbox" bind:value={content}></textarea> | ||||
|     <div class="composer-footer"> | ||||
|  |  | |||
|  | @ -3,9 +3,12 @@ | |||
|     import { server, createServer } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     import Logo from '$lib/../img/campfire-logo.svg'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     let display_error = false; | ||||
|     let logging_in = false; | ||||
| 
 | ||||
|  | @ -17,21 +20,21 @@ | |||
|         const host = event.target.host.value; | ||||
| 
 | ||||
|         if (!host || host === "") { | ||||
|             display_error = "Please enter an server domain."; | ||||
|             display_error = lang.string('login.error.no_domain'); | ||||
|             logging_in = false; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         server.set(await createServer(host)); | ||||
|         if (!get(server)) { | ||||
|             display_error = "Failed to connect to the server.\nCheck the browser console for details!" | ||||
|             display_error = lang.string('login.error.connection_failed'); | ||||
|             logging_in = false; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         app.set(await api.createApp(get(server).host)); | ||||
|         if (!get(app)) { | ||||
|             display_error = "Failed to create an application for this server." | ||||
|             display_error = lang.string('login.error.create_app'); | ||||
|             logging_in = false; | ||||
|             return; | ||||
|         } | ||||
|  | @ -44,8 +47,8 @@ | |||
|     <div class="app-logo"> | ||||
|         <Logo /> | ||||
|     </div> | ||||
|     <p>Welcome, fediverse user!</p> | ||||
|     <p>Please enter your server domain to log in.</p> | ||||
|     <p>{lang.string('login.welcome')}</p> | ||||
|     <p>{lang.string('login.enter_domain')}</p> | ||||
|     <div class="input-wrapper"> | ||||
|         <input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}> | ||||
|         {#if display_error} | ||||
|  | @ -53,16 +56,10 @@ | |||
|         {/if} | ||||
|     </div> | ||||
|     <br> | ||||
|     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button> | ||||
|     <p><small> | ||||
|         Please note this is | ||||
|         <strong><em>extremely experimental software</em></strong>; | ||||
|         things are likely to break! | ||||
|         <br> | ||||
|         If that's all cool with you, welcome aboard! | ||||
|     </small></p> | ||||
|     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>{lang.string('login.button')}</button> | ||||
|     <p><small>{@html lang.string('login.experimental')}</small></p> | ||||
| 
 | ||||
|     <p class="form-footer">made with ❤ by <a href="https://bliss.town">bliss town</a>, 2024</p> | ||||
|     <p class="form-footer">{@html lang.string('login.made_with_tagline')}</p> | ||||
| </form> | ||||
| 
 | ||||
| <style> | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ | |||
|         z-index: 101; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         position: absolute; | ||||
|         position: fixed; | ||||
|         width: 100vw; | ||||
|         height: 100vh; | ||||
|         pointer-events: none; | ||||
|  | @ -38,7 +38,7 @@ | |||
|         z-index: 101; | ||||
| 
 | ||||
|         padding: 16px; | ||||
|         width: 732px; | ||||
|         width: 700px; | ||||
|         border-radius: 8px; | ||||
|         box-shadow: 0px 16px 64px 4px rgba(0,0,0,0.5); | ||||
|         animation: modal_pop_up .15s cubic-bezier(0.22, 1, 0.36, 1); | ||||
|  | @ -50,7 +50,7 @@ | |||
|     .overlay { | ||||
|         width: 100vw; | ||||
|         height: 100vw; | ||||
|         position: absolute; | ||||
|         position: fixed; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         z-index: 100; | ||||
|  |  | |||
|  | @ -4,62 +4,45 @@ | |||
|     import { server } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { playSound } from '$lib/sound.js'; | ||||
|     import { getTimeline } from '$lib/timeline.js'; | ||||
|     import { getNotifications } from '$lib/notifications.js'; | ||||
|     import { goto } from '$app/navigation'; | ||||
|     import { page } from '$app/stores'; | ||||
|     import { createEventDispatcher } from 'svelte'; | ||||
|     import { notifications, unread_notif_count } from '$lib/notifications.js'; | ||||
|     import { createEventDispatcher, onMount } from 'svelte'; | ||||
|     import { unread_notif_count } from '$lib/notifications.js'; | ||||
|     import { fetchFollowRequests, followRequests } from '$lib/followRequests.js' | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     import Logo from '$lib/../img/campfire-logo.svg'; | ||||
|     import Button from './Button.svelte'; | ||||
| 
 | ||||
|     import TimelineIcon from '../../img/icons/timeline.svg'; | ||||
|     import NotificationsIcon from '../../img/icons/notifications.svg'; | ||||
|     import ExploreIcon from '../../img/icons/explore.svg'; | ||||
|     import ListIcon from '../../img/icons/lists.svg'; | ||||
|     import FavouritesIcon from '../../img/icons/like_fill.svg'; | ||||
|     import BookmarkIcon from '../../img/icons/bookmark.svg'; | ||||
|     import HashtagIcon from '../../img/icons/hashtag.svg'; | ||||
|     import PostIcon from '../../img/icons/post.svg'; | ||||
|     import InfoIcon from '../../img/icons/info.svg'; | ||||
|     import SettingsIcon from '../../img/icons/settings.svg'; | ||||
|     import LogoutIcon from '../../img/icons/logout.svg'; | ||||
|     import TimelineIcon from '@cf/icons/timeline.svg'; | ||||
|     import NotificationsIcon from '@cf/icons/notifications.svg'; | ||||
|     import ExploreIcon from '@cf/icons/explore.svg'; | ||||
|     import ListIcon from '@cf/icons/lists.svg'; | ||||
|     import FavouritesIcon from '@cf/icons/like_fill.svg'; | ||||
|     import BookmarkIcon from '@cf/icons/bookmark.svg'; | ||||
|     import HashtagIcon from '@cf/icons/hashtag.svg'; | ||||
|     import PostIcon from '@cf/icons/post.svg'; | ||||
|     import InfoIcon from '@cf/icons/info.svg'; | ||||
|     import SettingsIcon from '@cf/icons/settings.svg'; | ||||
|     import LogoutIcon from '@cf/icons/logout.svg'; | ||||
|     import FollowersIcon from '@cf/icons/followers.svg'; | ||||
| 
 | ||||
|     const VERSION = APP_VERSION; | ||||
| 
 | ||||
|     const COMMIT = APP_COMMIT; | ||||
|     const lang = Lang(); | ||||
|     const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
|     function handle_btn(name) { | ||||
|     function gotoProfile() { | ||||
|         if (!$account) return; | ||||
|         let route; | ||||
|         switch (name) { | ||||
|             case "timeline": | ||||
|                 route = "/"; | ||||
|                 getTimeline(true); | ||||
|                 break; | ||||
|             case "notifications": | ||||
|                 route = "/notifications"; | ||||
|                 notifications.set([]); | ||||
|                 getNotifications(); | ||||
|                 break; | ||||
|             case "explore": | ||||
|             case "lists": | ||||
|             case "favourites": | ||||
|             case "bookmarks": | ||||
|             case "hashtags": | ||||
|             default: | ||||
|                 return; | ||||
|         } | ||||
|         if (!route) return; | ||||
|         playSound(); | ||||
|         window.scrollTo({ | ||||
|             top: 0, | ||||
|             behavior: "smooth" | ||||
|         }); | ||||
|         goto(route); | ||||
|         goto(`/${$server.host}/${$account.username}`); | ||||
|     } | ||||
| 
 | ||||
|     async function log_out() { | ||||
|     async function logOut() { | ||||
|         if (!confirm("This will log you out. Are you sure?")) return; | ||||
|          | ||||
|         const res = await api.revokeToken( | ||||
|  | @ -70,7 +53,7 @@ | |||
|         ); | ||||
| 
 | ||||
|         if (!res.ok) | ||||
|             console.warn("Token revocation failed! Dumping data anyways"); | ||||
|             console.warn(lang.string('logs.token_revoke_failed')); | ||||
| 
 | ||||
|         account.set(false); | ||||
|         app.set(false); | ||||
|  | @ -78,6 +61,14 @@ | |||
| 
 | ||||
|         goto("/"); | ||||
|     } | ||||
| 
 | ||||
|     // fetch follow requests | ||||
|     let frqsFetched = false; | ||||
|      | ||||
|     $: if($account && !frqsFetched) { | ||||
|         fetchFollowRequests(true); | ||||
|         frqsFetched = true; | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <div id="navigation"> | ||||
|  | @ -89,79 +80,95 @@ | |||
| 
 | ||||
|     {#if $account} | ||||
|     <div id="nav-items"> | ||||
|         <Button label="Timeline" | ||||
|                 on:click={() => handle_btn("timeline")} | ||||
|         <Button label="{lang.string('navigation.timeline')}" | ||||
|                 href="/")} | ||||
|                 active={$page.url.pathname === "/"}> | ||||
|             <svelte:fragment slot="icon"> | ||||
|                 <TimelineIcon/> | ||||
|             </svelte:fragment> | ||||
|             Timeline | ||||
|             {lang.string('navigation.timeline')} | ||||
|         </Button> | ||||
|         <Button label="Notifications" | ||||
|                 on:click={() => handle_btn("notifications")} | ||||
|         <Button label="{lang.string('navigation.notifications')}" | ||||
|                 href="/notifications"} | ||||
|                 active={$page.url.pathname === "/notifications"}> | ||||
|             <svelte:fragment slot="icon"> | ||||
|                 <NotificationsIcon/> | ||||
|             </svelte:fragment> | ||||
|             Notifications | ||||
|             {lang.string('navigation.notifications')} | ||||
|             {#if $unread_notif_count} | ||||
|                 <span class="notification-count"> | ||||
|                     {$unread_notif_count <= 99 ? $unread_notif_count : "99+"} | ||||
|                 </span> | ||||
|             {/if} | ||||
|         </Button> | ||||
|         <Button label="Explore" disabled> | ||||
|         {#if $followRequests && $followRequests.length > 0} | ||||
|             <Button label="{lang.string('navigation.follow_requests')}" | ||||
|                     href="/follow-requests"} | ||||
|                     active={$page.url.pathname === "/follow-requests"}> | ||||
|                 <svelte:fragment slot="icon"> | ||||
|                     <FollowersIcon/> | ||||
|                 </svelte:fragment> | ||||
|                 {lang.string('navigation.follow_requests')} | ||||
|                 <span class="notification-count"> | ||||
|                     {$followRequests.length} | ||||
|                 </span> | ||||
|             </Button> | ||||
|         {/if} | ||||
|         <Button label="{lang.string('navigation.explore')}" disabled> | ||||
|             <svelte:fragment slot="icon"> | ||||
|                 <ExploreIcon height="auto"/> | ||||
|             </svelte:fragment> | ||||
|             Explore | ||||
|             {lang.string('navigation.explore')} | ||||
|         </Button> | ||||
|         <Button label="Lists" disabled> | ||||
|         <Button label="{lang.string('navigation.lists')}" disabled> | ||||
|             <svelte:fragment slot="icon"> | ||||
|                 <ListIcon/> | ||||
|             </svelte:fragment> | ||||
|             Lists | ||||
|             {lang.string('navigation.lists')} | ||||
|         </Button> | ||||
| 
 | ||||
|         <div class="flex-row"> | ||||
|             <Button centered label="Favourites" disabled> | ||||
|             <Button centered  | ||||
|             label="{lang.string('navigation.favourites')}"  | ||||
|             href="/favourites"} | ||||
|             active={$page.url.pathname === "/favourites"}> | ||||
|                 <svelte:fragment slot="icon"> | ||||
|                     <FavouritesIcon/> | ||||
|                 </svelte:fragment> | ||||
|             </Button> | ||||
|             <Button centered label="Bookmarks" disabled> | ||||
|             <Button centered label="{lang.string('navigation.bookmarks')}" disabled> | ||||
|                 <svelte:fragment slot="icon"> | ||||
|                     <BookmarkIcon/> | ||||
|                 </svelte:fragment> | ||||
|             </Button> | ||||
|             <Button centered label="Hashtags" disabled> | ||||
|             <Button centered label="{lang.string('navigation.hashtags')}" disabled> | ||||
|                 <svelte:fragment slot="icon"> | ||||
|                     <HashtagIcon/> | ||||
|                 </svelte:fragment> | ||||
|             </Button> | ||||
|         </div> | ||||
| 
 | ||||
|         <Button filled label="Post" on:click={() => dispatch("compose")}> | ||||
|         <Button filled label="{lang.string('compose')}" on:click={() => dispatch("compose")}> | ||||
|             <svelte:fragment slot="icon"> | ||||
|                 <PostIcon/> | ||||
|             </svelte:fragment> | ||||
|             Post | ||||
|             {lang.string('compose')} | ||||
|         </Button> | ||||
|     </div> | ||||
| 
 | ||||
|     <div id="account-items"> | ||||
|         <div class="flex-row"> | ||||
|             <Button centered label="Profile information" disabled> | ||||
|             <Button centered label="{lang.string('navigation.profile_information')}" disabled> | ||||
|                 <svelte:fragment slot="icon"> | ||||
|                     <InfoIcon/> | ||||
|                 </svelte:fragment> | ||||
|             </Button> | ||||
|             <Button centered label="Settings" disabled> | ||||
|             <Button centered label="{lang.string('navigation.settings')}" disabled> | ||||
|                 <svelte:fragment slot="icon"> | ||||
|                     <SettingsIcon/> | ||||
|                 </svelte:fragment> | ||||
|             </Button> | ||||
|             <Button centered label="Log out" on:click={() => log_out()}> | ||||
|             <Button centered label="{lang.string('navigation.log_out')}" on:click={() => logOut()}> | ||||
|                 <svelte:fragment slot="icon"> | ||||
|                     <LogoutIcon/> | ||||
|                 </svelte:fragment> | ||||
|  | @ -169,11 +176,11 @@ | |||
|         </div> | ||||
| 
 | ||||
|         <div id="account-button"> | ||||
|             <img src={$account.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => playSound()}> | ||||
|             <img src={$account.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => gotoProfile()}> | ||||
|             <div class="account-name" aria-hidden="true"> | ||||
|                 <a href={$account.url} class="nickname" title={$account.nickname}>{@html $account.rich_name}</a> | ||||
|                 <a href="/{$server.host}/@{$account.username}" class="nickname" title={$account.nickname}>{@html $account.rich_name}</a> | ||||
|                 <span class="username" title={`@${$account.username}@${$account.host}`}> | ||||
|                     {`@${$account.username}@${$account.host}`} | ||||
|                     {$account.fqn} | ||||
|                 </span> | ||||
|             </div> | ||||
|         </div> | ||||
|  | @ -181,11 +188,11 @@ | |||
|     {/if} | ||||
| 
 | ||||
|     <span class="version"> | ||||
|         campfire v{VERSION} | ||||
|         campfire v{VERSION} ({COMMIT}) | ||||
|         <br> | ||||
|         <ul> | ||||
|             <li><a href="https://git.arimelody.me/blisstown/campfire">source</a></li> | ||||
|             <li><a href="https://github.com/blisstown/campfire/issues">issues</a></li> | ||||
|             <li><a href="https://git.arimelody.me/blisstown/campfire">{lang.string('source')}</a></li> | ||||
|             <li><a href="https://codeberg.org/arimelody/campfire/issues">{lang.string('issues')}</a></li> | ||||
|         </ul> | ||||
|     </span> | ||||
| </div> | ||||
|  | @ -200,6 +207,8 @@ | |||
|         height: calc(100vh - 32px); | ||||
|         border-radius: 8px; | ||||
|         background-color: var(--bg-800); | ||||
|         transition: background-color .1s linear; | ||||
|         user-select: none; | ||||
|     } | ||||
| 
 | ||||
|     .server-header { | ||||
|  | @ -213,6 +222,7 @@ | |||
|         background-size: cover; | ||||
|         background-color: var(--bg-600); | ||||
|         background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); | ||||
|         transition: background .1s linear; | ||||
|     } | ||||
| 
 | ||||
|     .server-icon { | ||||
|  | @ -270,6 +280,7 @@ | |||
|         font-size: .9em; | ||||
|         opacity: .6; | ||||
|         text-align: center; | ||||
|         user-select: text; | ||||
|     } | ||||
| 
 | ||||
|     .version ul { | ||||
|  |  | |||
|  | @ -2,17 +2,20 @@ | |||
|     import { server } from '$lib/client/server'; | ||||
|     import { goto } from '$app/navigation'; | ||||
| 
 | ||||
|     import ReplyIcon from '$lib/../img/icons/reply.svg'; | ||||
|     import RepostIcon from '$lib/../img/icons/repost.svg'; | ||||
|     import FavouriteIcon from '$lib/../img/icons/like.svg'; | ||||
|     import ReactIcon from '$lib/../img/icons/react.svg'; | ||||
|     import ReplyIcon from '@cf/icons/reply.svg'; | ||||
|     import RepostIcon from '@cf/icons/repost.svg'; | ||||
|     import FavouriteIcon from '@cf/icons/like.svg'; | ||||
|     import ReactIcon from '@cf/icons/react.svg'; | ||||
|     // import QuoteIcon from '$lib/../img/icons/quote.svg'; | ||||
|     import ReactionBar from '$lib/ui/post/ReactionBar.svelte'; | ||||
|     import ActionBar from '$lib/ui/post/ActionBar.svelte'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     let mention = (accounts) => { | ||||
|         let res = `<a href=${account.url}>${account.rich_name}</a>`; | ||||
|         if (accounts.length > 1) res += ` and <strong>${accounts.length - 1}</strong> others`; | ||||
|         let res = `<a href="/${$server.host}/${account.fqn}">${account.rich_name}</a>`; | ||||
|         if (accounts.length > 1) res += ' ' + lang.string('notification.and_others').replaceAll('%1', accounts.length - 1); | ||||
|         return res; | ||||
|     }; | ||||
| 
 | ||||
|  | @ -20,23 +23,23 @@ | |||
|     let activity_text = function (type) { | ||||
|         switch (type) { | ||||
|             case "mention": | ||||
|                 return `%1 mentioned you.`; | ||||
|                 return lang.string('notification.mention'); | ||||
|             case "reblog": | ||||
|                 return `%1 boosted your post.`; | ||||
|                 return lang.string('notification.reblog'); | ||||
|             case "reaction": | ||||
|                 return `%1 reacted to your post.`; | ||||
|                 return lang.string('notification.reaction'); | ||||
|             case "follow": | ||||
|                 return `%1 followed you.`; | ||||
|                 return lang.string('notification.follow'); | ||||
|             case "follow_request": | ||||
|                 return `%1 requested to follow you.`; | ||||
|                 return lang.string('notification.follow.request'); | ||||
|             case "favourite": | ||||
|                 return `%1 favourited your post.`; | ||||
|                 return lang.string('notification.favourite'); | ||||
|             case "poll": | ||||
|                 return `%1's poll as ended.`; | ||||
|                 return lang.string('notification.poll'); | ||||
|             case "update": | ||||
|                 return `%1 updated their post.`; | ||||
|                 return lang.string('notification.update'); | ||||
|             default: | ||||
|                 return `%1 poked you!`; | ||||
|                 return lang.string('notification.default'); | ||||
|         } | ||||
|     }(data.type); | ||||
| 
 | ||||
|  | @ -87,7 +90,7 @@ | |||
|         </span> | ||||
|         <span class="notif-avatars"> | ||||
|             {#if data.accounts.length == 1} | ||||
|                 <a href={data.accounts[0].url} class="notif-avatar"> | ||||
|                 <a href="/{$server.host}/{data.accounts[0].fqn}" class="notif-avatar"> | ||||
|                     <img src={data.accounts[0].avatar_url} alt="" width="28" height="28" /> | ||||
|                 </a> | ||||
|             {:else} | ||||
|  | @ -141,18 +144,22 @@ | |||
| <style> | ||||
|     .notification { | ||||
|         display: block; | ||||
|         margin-bottom: 8px; | ||||
|         border-top: 1px solid color-mix(in srgb, transparent, var(--text) 25%); | ||||
|         padding: 16px; | ||||
|         border-radius: 8px; | ||||
|         background: var(--bg-800); | ||||
|         text-decoration: inherit; | ||||
|         color: inherit; | ||||
|         transition: background-color .1s; | ||||
|         cursor: pointer; | ||||
| 
 | ||||
|         background-color: var(--bg-900); | ||||
|     } | ||||
|     .notification:first-of-type { | ||||
|         border-top: none; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     .notification:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||
|         background-color: color-mix(in srgb, var(--bg-800), transparent 35%); | ||||
|     } | ||||
| 
 | ||||
|     header { | ||||
|  | @ -280,7 +287,7 @@ | |||
|         width: calc(100% - 16px); | ||||
|         margin-bottom: 10px; | ||||
|         padding: 4px 8px; | ||||
|         --warn-bg: color-mix(in srgb, var(--bg-700), var(--accent) 1%); | ||||
|         --warn-bg: color-mix(in srgb, transparent, var(--bg-700) 50%); | ||||
|         background: repeating-linear-gradient(-45deg, transparent, transparent 10px, var(--warn-bg) 10px, var(--warn-bg) 20px); | ||||
|         font-family: inherit; | ||||
|         font-size: inherit; | ||||
|  |  | |||
|  | @ -1,5 +1,11 @@ | |||
| <script> | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| </script> | ||||
| 
 | ||||
| <div id="widgets"> | ||||
|     <input type="text" id="search" placeholder="Search"> | ||||
|     <input type="text" id="search" placeholder="{lang.string('search')}"> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|  |  | |||
							
								
								
									
										36
									
								
								src/lib/ui/core/PageHeader.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/lib/ui/core/PageHeader.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| <script> | ||||
|     export let title; | ||||
| </script> | ||||
| 
 | ||||
| <header> | ||||
|     <h1>{title}</h1> | ||||
|     <div class="header-items"> | ||||
|         <slot/> | ||||
|     </div> | ||||
| </header> | ||||
| 
 | ||||
| <style> | ||||
|     header { | ||||
|         width: 100%; | ||||
|         height: 64px; | ||||
|         margin: 16px 0; | ||||
|         padding: 0 8px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         user-select: none; | ||||
|         box-sizing: border-box; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     header h1 { | ||||
|         font-size: 1.5em; | ||||
|     } | ||||
| 
 | ||||
|     header .header-items { | ||||
|         margin-left: auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|         gap: 8px; | ||||
|     } | ||||
| </style> | ||||
|  | @ -6,19 +6,22 @@ | |||
|     import { timeline } from '$lib/timeline'; | ||||
|     import { parseReactions } from '$lib/post'; | ||||
|     import { playSound } from '$lib/sound'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     import ActionButton from './ActionButton.svelte'; | ||||
| 
 | ||||
|     import ReplyIcon from '../../../img/icons/reply.svg'; | ||||
|     import RepostIcon from '../../../img/icons/repost.svg'; | ||||
|     import FavouriteIcon from '../../../img/icons/like.svg'; | ||||
|     import FavouriteIconFill from '../../../img/icons/like_fill.svg'; | ||||
|     import QuoteIcon from '../../../img/icons/quote.svg'; | ||||
|     import MoreIcon from '../../../img/icons/more.svg'; | ||||
|     import DeleteIcon from '../../../img/icons/bin.svg'; | ||||
|     import ReplyIcon from '@cf/icons/reply.svg'; | ||||
|     import RepostIcon from '@cf/icons/repost.svg'; | ||||
|     import FavouriteIcon from '@cf/icons/like.svg'; | ||||
|     import FavouriteIconFill from '@cf/icons/like_fill.svg'; | ||||
|     import QuoteIcon from '@cf/icons/quote.svg'; | ||||
|     import MoreIcon from '@cf/icons/more.svg'; | ||||
|     import DeleteIcon from '@cf/icons/bin.svg'; | ||||
| 
 | ||||
|     export let post; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     async function toggleBoost() { | ||||
|         if (!$app || !$app.token) return; | ||||
| 
 | ||||
|  | @ -74,29 +77,29 @@ | |||
| </script> | ||||
| 
 | ||||
| <div class="post-actions" aria-label="Post actions" role="toolbar" tabindex="0" on:mouseup|stopPropagation on:keydown|stopPropagation> | ||||
|     <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled> | ||||
|     <ActionButton type="reply" label="{lang.string('post.actions.reply')}" bind:count={post.reply_count} sound="post" disabled> | ||||
|         <ReplyIcon/> | ||||
|     </ActionButton> | ||||
|     <ActionButton type="boost" label="Boost" on:click={toggleBoost} bind:active={post.boosted} bind:count={post.boost_count} disabled={!$account}> | ||||
|     <ActionButton type="boost" label="{lang.string('post.actions.boost')}" on:click={toggleBoost} bind:active={post.boosted} bind:count={post.boost_count} disabled={!$account}> | ||||
|         <RepostIcon/> | ||||
|         <svelte:fragment slot="activeIcon"> | ||||
|             <RepostIcon/> | ||||
|         </svelte:fragment> | ||||
|     </ActionButton> | ||||
|     <ActionButton type="favourite" label="Favourite" on:click={toggleFavourite} bind:active={post.favourited} bind:count={post.favourite_count} disabled={!$account}> | ||||
|     <ActionButton type="favourite" label="{lang.string('post.actions.favourite')}" on:click={toggleFavourite} bind:active={post.favourited} bind:count={post.favourite_count} disabled={!$account}> | ||||
|         <FavouriteIcon/> | ||||
|         <svelte:fragment slot="activeIcon"> | ||||
|             <FavouriteIconFill/> | ||||
|         </svelte:fragment> | ||||
|     </ActionButton> | ||||
|     <ActionButton type="quote" label="Quote" disabled> | ||||
|     <ActionButton type="quote" label="{lang.string('post.actions.quote')}" disabled> | ||||
|         <QuoteIcon/> | ||||
|     </ActionButton> | ||||
|     <ActionButton type="more" label="More" disabled> | ||||
|     <ActionButton type="more" label="{lang.string('post.actions.more')}" disabled> | ||||
|         <MoreIcon/> | ||||
|     </ActionButton> | ||||
|     {#if $account && post.account.id === $account.id} | ||||
|         <ActionButton type="delete" label="Delete" on:click={deletePost}> | ||||
|         <ActionButton type="delete" label="{lang.string('post.actions.delete')}" on:click={deletePost}> | ||||
|             <DeleteIcon/> | ||||
|         </ActionButton> | ||||
|     {/if} | ||||
|  |  | |||
|  | @ -1,25 +1,29 @@ | |||
| <script> | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     export let post; | ||||
| 
 | ||||
|     let open_warned = false; | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     let open = false; | ||||
| </script> | ||||
| 
 | ||||
| <div class="post-body"> | ||||
|     {#if post.warning} | ||||
|         <button class="post-warning" on:click|stopPropagation={() => { open_warned = !open_warned }} on:mouseup|stopPropagation> | ||||
|         <button class="post-warning" on:click|stopPropagation={() => { open = !open }} on:mouseup|stopPropagation> | ||||
|         <strong> | ||||
|             {post.warning} | ||||
|             <span class="warning-instructions"> | ||||
|                 {#if !open_warned} | ||||
|                     (click to reveal) | ||||
|             <span class="instructions"> | ||||
|                 {#if !open} | ||||
|                     {lang.string('post.warning.show')} | ||||
|                 {:else} | ||||
|                     (click to hide) | ||||
|                     {lang.string('post.warning.hide')} | ||||
|                 {/if} | ||||
|             </span> | ||||
|         </strong> | ||||
|         </button> | ||||
|     {/if} | ||||
|     {#if !post.warning || open_warned} | ||||
|     {#if !post.warning || open} | ||||
|         {#if post.rich_text} | ||||
|             <span class="post-text">{@html post.rich_text}</span> | ||||
|         {:else if post.html} | ||||
|  | @ -60,7 +64,7 @@ | |||
|         width: 100%; | ||||
|         margin-bottom: 10px; | ||||
|         padding: 4px 8px; | ||||
|         --warn-bg: color-mix(in srgb, var(--bg-700), var(--accent) 1%); | ||||
|         --warn-bg: color-mix(in srgb, transparent, var(--bg-700) 50%); | ||||
|         background: repeating-linear-gradient(-45deg, transparent, transparent 10px, var(--warn-bg) 10px, var(--warn-bg) 20px); | ||||
|         font-family: inherit; | ||||
|         font-size: inherit; | ||||
|  | @ -78,13 +82,12 @@ | |||
|         box-shadow: 0 0 8px var(--warn-bg); | ||||
|     } | ||||
| 
 | ||||
|     .post-warning .warning-instructions { | ||||
|     .post-warning .instructions { | ||||
|         font-weight: normal; | ||||
|         opacity: .5; | ||||
|     } | ||||
| 
 | ||||
|     .post-text { | ||||
|         font-size: .9em; | ||||
|         line-height: 1.45em; | ||||
|         word-wrap: break-word; | ||||
|     } | ||||
|  | @ -129,7 +132,9 @@ | |||
|         color: var(--accent); | ||||
|     } | ||||
| 
 | ||||
|     .post-text :global(a.mention) { | ||||
|     /* mention gets used in other places (bios) so it's global */ | ||||
|      | ||||
|     :global(a.mention) { | ||||
|         color: inherit; | ||||
|         font-weight: 600; | ||||
|         padding: 3px 6px; | ||||
|  | @ -138,7 +143,7 @@ | |||
|         text-decoration: none; | ||||
|     } | ||||
| 
 | ||||
|     .post-text :global(a.mention:hover) { | ||||
|     :global(a.mention:hover) { | ||||
|         text-decoration: underline; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,10 @@ | |||
| <script> | ||||
|     import { shorthand as short_time } from '$lib/time.js'; | ||||
|     import { server } from '$lib/client/server'; | ||||
|     import RepostIcon from '@cf/icons/repost.svg'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     export let post; | ||||
| 
 | ||||
|  | @ -7,17 +12,19 @@ | |||
| </script> | ||||
| 
 | ||||
| <div class="post-context"> | ||||
|     <span class="post-context-icon">🔁</span> | ||||
|     <span class="post-context-icon"> | ||||
|         <RepostIcon width="22px" /> | ||||
|     </span> | ||||
|     <span class="post-context-action"> | ||||
|         <a href={post.account.url} target="_blank"><span class="name"> | ||||
|                 {@html post.account.rich_name}</span> | ||||
|         </a> | ||||
|         boosted this post. | ||||
|         { @html | ||||
|         lang.string('post.boosted', | ||||
|         `<a href="/${$server.host}/${post.account.fqn}"><span class="name">${post.account.rich_name}</span></a>`) | ||||
|         } | ||||
|     </span> | ||||
|     <span class="post-context-time"> | ||||
|         <time title="{time_string}">{short_time(post.created_at)}</time> | ||||
|         {#if post.visibility !== "public"} | ||||
|             <span class="post-visibility">- {post.visibility}</span> | ||||
|             <span class="post-visibility">- {lang.string(`post.visibility.${post.visibility}`)}</span> | ||||
|         {/if} | ||||
|     </span> | ||||
| </div> | ||||
|  | @ -38,11 +45,13 @@ | |||
|     } | ||||
| 
 | ||||
|     .post-context-icon { | ||||
|         margin-right: 4px; | ||||
|         height: 1em; | ||||
|         margin-right: .1em; | ||||
|         transform: translateY(-.3em); | ||||
|     } | ||||
| 
 | ||||
|     .post-context a, | ||||
|     .post-context a:visited { | ||||
|     :global(.post-context a), | ||||
|     :global(.post-context a:visited) { | ||||
|         color: inherit; | ||||
|         text-decoration: none; | ||||
|     } | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
|     import { onMount } from 'svelte'; | ||||
|     import { goto } from '$app/navigation'; | ||||
|     import { server } from '$lib/client/server'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     import BoostContext from './BoostContext.svelte'; | ||||
|     import ReplyContext from './ReplyContext.svelte'; | ||||
|  | @ -12,6 +13,9 @@ | |||
| 
 | ||||
|     export let post_data; | ||||
|     export let focused = false; | ||||
|     export let pinned = false; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     let post_context = undefined; | ||||
|     let post = post_data; | ||||
|  | @ -41,8 +45,6 @@ | |||
|             window.scrollTo(0, el.scrollHeight); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     let aria_label = post.account.username + '; ' + post.text + '; ' + post.created_at; | ||||
| </script> | ||||
| 
 | ||||
| <div class="post-container"> | ||||
|  | @ -51,12 +53,15 @@ | |||
|             <ReplyContext post={reply} /> | ||||
|         {/await} | ||||
|     {/if} | ||||
|     {#if pinned} | ||||
|         <p class="post-context pinned">{lang.string('post.pinned')}</p> | ||||
|     {/if} | ||||
|     {#if is_boost && !post_context.text} | ||||
|         <BoostContext post={post_context} /> | ||||
|     {/if} | ||||
|     <article | ||||
|             class={"post" + (focused ? " focused" : "")} | ||||
|             aria-label={aria_label} | ||||
|             aria-label={post.account.username + '; ' + post.text + '; ' + post.created_at} | ||||
|             bind:this={el} | ||||
|             on:mousedown={e => {mouse_pos.left = e.pageX; mouse_pos.top = e.pageY}} | ||||
|             on:mouseup={e => {if (e.pageX == mouse_pos.left && e.pageY == mouse_pos.top) gotoPost(e)}} | ||||
|  | @ -74,18 +79,24 @@ | |||
| 
 | ||||
| <style> | ||||
|     .post-container { | ||||
|         width: 732px; | ||||
|         max-width: 732px; | ||||
|         margin-bottom: 8px; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         border-radius: 8px; | ||||
|         background-color: var(--bg-800); | ||||
|         border-top: 1px solid color-mix(in srgb, transparent, var(--text) 20%); | ||||
|         background: var(--bg-900); | ||||
|     } | ||||
|     .post-container:first-of-type { | ||||
|         border-top: none; | ||||
|     } | ||||
| 
 | ||||
|     .pinned { | ||||
|         margin: .9em 1.2em .3em 1.2em; | ||||
|         font-size: .8em; | ||||
|         color: var(--accent); | ||||
|         background-color: inherit; | ||||
|     } | ||||
| 
 | ||||
|     .post { | ||||
|         padding: 16px; | ||||
|         border-radius: 8px; | ||||
|         transition: background-color .1s; | ||||
|         cursor: pointer; | ||||
|     } | ||||
|  | @ -100,7 +111,7 @@ | |||
|     } | ||||
| 
 | ||||
|     .post:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||
|         background-color: color-mix(in srgb, transparent, var(--bg-800) 25%); | ||||
|     } | ||||
| 
 | ||||
|     .post-container:has(.post-context) .post { | ||||
|  |  | |||
|  | @ -1,5 +1,9 @@ | |||
| <script> | ||||
|     import { shorthand as short_time } from '$lib/time.js'; | ||||
|     import { server } from '$lib/client/server'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     export let post; | ||||
|     export let reply = undefined; | ||||
|  | @ -8,21 +12,19 @@ | |||
| </script> | ||||
| 
 | ||||
| <div class={"post-header-container" + (reply ? " reply" : "")}> | ||||
|     <a href={post.account.url} target="_blank" class="post-avatar-container" on:mouseup|stopPropagation> | ||||
|     <a href="/{$server.host}/@{post.account.fqn}" class="post-avatar-container" on:mouseup|stopPropagation> | ||||
|         <img src={post.account.avatar_url} type={post.account.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> | ||||
|     </a> | ||||
|     <header class="post-header"> | ||||
|         <div class="post-user-info" on:mouseup|stopPropagation> | ||||
|             <a href={post.account.url} target="_blank" class="name">{@html post.account.rich_name}</a> | ||||
|             <a href="/{$server.host}/@{post.account.fqn}" class="name">{@html post.account.rich_name}</a> | ||||
|             <span class="username">{post.account.mention}</span> | ||||
|         </div> | ||||
|         <div class="post-info" on:mouseup|stopPropagation> | ||||
|             <a href={post.url} target="_blank" class="created-at"> | ||||
|                 <time title={time_string}>{short_time(post.created_at)}</time> | ||||
|                 {#if post.visibility !== "public"} | ||||
|                 <br> | ||||
|                     <span class="post-visibility">{post.visibility}</span> | ||||
|                 {/if} | ||||
|                 <span class="post-visibility">{lang.string('post.visibility.' + post.visibility)}</span> | ||||
|             </a> | ||||
|         </div> | ||||
|     </header> | ||||
|  |  | |||
|  | @ -51,11 +51,7 @@ | |||
|                 {/if} | ||||
|         </ReactionButton> | ||||
|     {/each} | ||||
|     <ReactionButton | ||||
|             type="reaction" | ||||
|             title="react" | ||||
|             label="React" | ||||
|             disabled> | ||||
|     <ReactionButton disabled> | ||||
|     <ReactIcon/> | ||||
|     </ReactionButton> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,11 +1,14 @@ | |||
| <script> | ||||
|     import { playSound } from '../../sound.js'; | ||||
|     import { createEventDispatcher } from 'svelte'; | ||||
|     import Lang from '$lib/lang'; | ||||
|     const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     export let type = "react"; | ||||
|     export let label = "React"; | ||||
|     export let title = label; | ||||
|     export let label = lang.string('post.actions.react'); | ||||
|     export let title = lang.string('post.actions.react'); | ||||
|     export let count = 0; | ||||
|     export let active = false; | ||||
|     export let disabled = false; | ||||
|  |  | |||
|  | @ -61,13 +61,12 @@ | |||
|         flex-direction: row; | ||||
|         color: var(--text); | ||||
|         align-items: stretch; | ||||
|         border-radius: 8px; | ||||
|         transition: background-color .1s; | ||||
|         cursor: pointer; | ||||
|     } | ||||
| 
 | ||||
|     .post-reply:hover { | ||||
|         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||
|         background-color: color-mix(in srgb, var(--bg-800), transparent 50%); | ||||
|     } | ||||
| 
 | ||||
|     .post-avatar-container { | ||||
|  |  | |||
							
								
								
									
										78
									
								
								src/lib/ui/timeline/Timeline.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/lib/ui/timeline/Timeline.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | |||
| <script> | ||||
|     import { page } from '$app/stores'; | ||||
|     import { account } from '$lib/stores/account.js'; | ||||
|     import { timeline, getTimeline, clearTimeline } from '$lib/timeline.js'; | ||||
|     import { app_name } from '$lib/config.js'; | ||||
|     import Post from '$lib/ui/post/Post.svelte'; | ||||
|      | ||||
|     import Lang from '$lib/lang'; | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     // TODO: refactor to enum when moving to TS | ||||
|     export let timelineType; | ||||
| 
 | ||||
|     $: { | ||||
|         // awful hack to update timeline fresh | ||||
|         // when timelineType is updated | ||||
|         // | ||||
|         // TODO: migrate to $effect when migrating to svelte 5 | ||||
|         timelineType = timelineType | ||||
| 
 | ||||
|         clearTimeline() | ||||
|         getCurrentTimeline() | ||||
|     } | ||||
| 
 | ||||
|     function getCurrentTimeline(clean = false) { | ||||
|         switch(timelineType) { | ||||
|             case "home": | ||||
|                 getTimeline("home", clean); | ||||
|                 break; | ||||
| 
 | ||||
|             case "local": | ||||
|                 getTimeline("public", clean, true) | ||||
|                 break; | ||||
| 
 | ||||
|             case "federated": | ||||
|                 getTimeline("public", clean, false, true) | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     account.subscribe(account => { | ||||
|         if (account) getCurrentTimeline(); | ||||
|     }); | ||||
| 
 | ||||
|     document.addEventListener('scroll', () => { | ||||
|         if ($account && $page.url.pathname !== "/") return; | ||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||
|             getCurrentTimeline(); | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
| <div id="feed" role="feed"> | ||||
|     {#if $timeline.length <= 0} | ||||
|         <div class="loading throb"> | ||||
|             <span>{lang.string('timeline.fetching')}</span> | ||||
|         </div> | ||||
|     {/if} | ||||
|     {#each $timeline as post} | ||||
|         <Post post_data={post} /> | ||||
|     {/each} | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <style> | ||||
|     #feed { | ||||
|         margin-bottom: 20vh; | ||||
|     } | ||||
| 
 | ||||
|     .loading { | ||||
|         width: 100%; | ||||
|         height: 80vh; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|         font-size: 2em; | ||||
|         font-weight: bold; | ||||
|     } | ||||
| </style> | ||||
|  | @ -1,17 +1,20 @@ | |||
| <script> | ||||
|     import '$lib/app.css'; | ||||
|     import '../app.css'; | ||||
|     import * as api from '$lib/api.js'; | ||||
|     import { server } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { account } from '$lib/stores/account.js'; | ||||
|     import { parseAccount } from '$lib/account.js'; | ||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     import Navigation from '$lib/ui/Navigation.svelte'; | ||||
|     import Modal from '@cf/ui/Modal.svelte'; | ||||
|     import Composer from '@cf/ui/Composer.svelte'; | ||||
|     import Widgets from '$lib/ui/Widgets.svelte'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     let show_composer = false; | ||||
| 
 | ||||
|     async function init() { | ||||
|  | @ -25,7 +28,7 @@ | |||
|         if (!data) return; | ||||
| 
 | ||||
|         account.set(parseAccount(data)); | ||||
|         console.log(`Logged in as @${$account.username}@${$account.host}`); | ||||
|         console.log(lang.string('logs.logged_in', $account.fqn)); | ||||
| 
 | ||||
|         // spin up async task to fetch notifications | ||||
|         const notif_data = await api.getNotifications( | ||||
|  | @ -48,7 +51,7 @@ | |||
|     <main> | ||||
|         {#await init()} | ||||
|             <div class="loading throb"> | ||||
|                 <span>just a moment...</span> | ||||
|                 <span>{lang.string('loading')}</span> | ||||
|             </div> | ||||
|         {:then} | ||||
|             <slot></slot> | ||||
|  |  | |||
|  | @ -2,70 +2,57 @@ | |||
|     import { page } from '$app/stores'; | ||||
|     import { account } from '$lib/stores/account.js'; | ||||
|     import { timeline, getTimeline } from '$lib/timeline.js'; | ||||
|     import { app_name } from '$lib/config.js'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     import LoginForm from '$lib/ui/LoginForm.svelte'; | ||||
|     import Button from '$lib/ui/Button.svelte'; | ||||
|     import Post from '$lib/ui/post/Post.svelte'; | ||||
|     import PageHeader from '../lib/ui/core/PageHeader.svelte'; | ||||
|     import Timeline from '../lib/ui/timeline/Timeline.svelte'; | ||||
| 
 | ||||
|     account.subscribe(account => { | ||||
|         if (account) getTimeline(); | ||||
|     }); | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     document.addEventListener("scroll", () => { | ||||
|         if ($account && $page.url.pathname !== "/") return; | ||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||
|             getTimeline(); | ||||
|     // TODO: refactor to enum when moving to TS | ||||
|     let timelineType = localStorage.getItem(app_name + '_selected_timeline') || "home"; | ||||
| 
 | ||||
|     $: { | ||||
|         // awful hack to update timeline fresh | ||||
|         // when timelineType is updated | ||||
|         // | ||||
|         // TODO: migrate to $effect when migrating to svelte 5 | ||||
|         timelineType = timelineType | ||||
| 
 | ||||
|         // set in localStorage | ||||
|         localStorage.setItem(app_name + '_selected_timeline', timelineType); | ||||
|     } | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| {#if $account} | ||||
|     <header> | ||||
|         <h1>Home</h1> | ||||
|         <nav> | ||||
|             <Button centered active>Home</Button> | ||||
|             <Button centered disabled>Local</Button> | ||||
|             <Button centered disabled>Federated</Button> | ||||
|         </nav> | ||||
|     </header> | ||||
|     <PageHeader title={lang.string(`timeline.${timelineType}`)}> | ||||
|         <Button centered  | ||||
|         active={(timelineType == "home")}  | ||||
|         on:click={() => timelineType = "home"}> | ||||
|             {lang.string('timeline.home')} | ||||
|         </Button> | ||||
|         <Button centered  | ||||
|         active={(timelineType == "local")}  | ||||
|         on:click={() => timelineType = "local"}> | ||||
|             {lang.string('timeline.local')} | ||||
|         </Button> | ||||
|                 <Button centered  | ||||
|         active={(timelineType == "federated")}  | ||||
|         on:click={() => timelineType = "federated"}> | ||||
|             {lang.string('timeline.federated')} | ||||
|         </Button> | ||||
|     </PageHeader> | ||||
| 
 | ||||
|     <div id="feed" role="feed"> | ||||
|         {#if $timeline.length <= 0} | ||||
|             <div class="loading throb"> | ||||
|                 <span>getting the feed...</span> | ||||
|             </div> | ||||
|         {/if} | ||||
|         {#each $timeline as post} | ||||
|             <Post post_data={post} /> | ||||
|         {/each} | ||||
|     </div> | ||||
|     <Timeline timelineType={timelineType}/> | ||||
| {:else} | ||||
|     <LoginForm /> | ||||
| {/if} | ||||
| 
 | ||||
| <style> | ||||
|     header { | ||||
|         width: 100%; | ||||
|         height: 64px; | ||||
|         margin: 16px 0 8px 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|     } | ||||
| 
 | ||||
|     header h1 { | ||||
|         font-size: 1.5em; | ||||
|     } | ||||
| 
 | ||||
|     nav { | ||||
|         margin-left: auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         gap: 8px; | ||||
|     } | ||||
| 
 | ||||
|     #feed { | ||||
|         margin-bottom: 20vh; | ||||
|     } | ||||
| 
 | ||||
|     .loading { | ||||
|         width: 100%; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| export async function load({ params }) { | ||||
|     return { | ||||
|         server_domain: params.server | ||||
|         server_host: params.server | ||||
|     }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +1,10 @@ | |||
| import { error } from '@sveltejs/kit'; | ||||
| 
 | ||||
| export async function load({ params }) { | ||||
|     return error(404, 'Not Found'); | ||||
|     // return {
 | ||||
|     //     account_name: params.account
 | ||||
|     // };
 | ||||
|     let handle = params.account; | ||||
|     if (handle.startsWith('@')) | ||||
|         handle = handle.substring(1); | ||||
| 
 | ||||
|     return { | ||||
|         server_host: params.server, | ||||
|         account_handle: handle, | ||||
|     }; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										330
									
								
								src/routes/[server]/[account]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								src/routes/[server]/[account]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,330 @@ | |||
| <script> | ||||
|     import Button from '@cf/ui/Button.svelte'; | ||||
|     import HomeIcon from '@cf/icons/unlisted.svg'; | ||||
|     import MoreIcon from '@cf/icons/more.svg'; | ||||
|     import LockIcon from '@cf/icons/lock.svg'; | ||||
|     import BotIcon from '@cf/icons/bot.svg'; | ||||
|     import Lang from '$lib/lang'; | ||||
|     import * as api from '$lib/api.js'; | ||||
|     import { server, createServer } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { parseAccount } from '$lib/account.js'; | ||||
|     import { parsePost } from '$lib/post.js'; | ||||
|     import { account } from '$lib/stores/account'; | ||||
|     import { goto, afterNavigate } from '$app/navigation'; | ||||
|     import { base } from '$app/paths'; | ||||
|     import Post from '../../../lib/ui/post/Post.svelte'; | ||||
|     import { writable } from 'svelte/store'; | ||||
| 
 | ||||
|     export let data; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     let profile_pinned_posts = writable([]); | ||||
|     let profile_posts_max_id = null; | ||||
|     let profile_posts = writable([]); | ||||
|     let profile = fetchProfile(data.account_handle); | ||||
|     let error = false; | ||||
|     let previous_page = base; | ||||
| 
 | ||||
|     let post_replies = false; | ||||
|     let post_boosts = true; | ||||
|     let post_media = false; | ||||
| 
 | ||||
|     afterNavigate(({from}) => { | ||||
|         previous_page = from?.url.pathname || previous_page; | ||||
|         profile = fetchProfile(data.account_handle); | ||||
|     }) | ||||
| 
 | ||||
|     async function getPosts(profile, max_id) { | ||||
|         const posts = await api.getUserPosts( | ||||
|             $server.host, | ||||
|             $app.token, | ||||
|             profile.id, | ||||
|             max_id, | ||||
|             post_replies, | ||||
|             post_boosts, | ||||
|             post_media | ||||
|         ); | ||||
|         let parsed_posts = []; | ||||
|         for (let post of posts) { | ||||
|             parsed_posts.push(await parsePost(post, 1)); | ||||
|         } | ||||
|         profile_posts.update(posts => { | ||||
|             posts.push(...parsed_posts); | ||||
|             return posts; | ||||
|         }); | ||||
|         return parsed_posts.length > 0 ? parsed_posts[parsed_posts.length - 1].id : null; | ||||
|     } | ||||
| 
 | ||||
|     async function fetchProfile(handle) { | ||||
|         let token = $app ? $app.token : null; | ||||
| 
 | ||||
|         profile_posts.set([]); | ||||
|         profile_pinned_posts.set([]); | ||||
| 
 | ||||
|         if (!$server || $server.host !== data.server_host) { | ||||
|             server.set(await createServer(data.server_host)); | ||||
|             if (!$server) { | ||||
|                 error = lang.string('error.connection_failed', data.server_host); | ||||
|                 throw new Error(lang.string('logs.connection_failed', data.server_host)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let profile_data; | ||||
|         try { | ||||
|             profile_data = await api.lookupUser($server.host, token, handle); | ||||
|         } catch (error) { | ||||
|             throw error; | ||||
|         } | ||||
| 
 | ||||
|         if (!profile_data || profile_data.error) { | ||||
|             error = lang.string('error.profile_fetch_failed_id', handle); | ||||
|             throw new Error(lang.string('logs.profile_fetch_failed_id', handle)); | ||||
|         } | ||||
|         let profile = await parseAccount(profile_data, 0); | ||||
| 
 | ||||
|         api.getUserPinnedPosts( | ||||
|             $server.host, | ||||
|             token, | ||||
|             profile.id, | ||||
|         ).then(async posts => { | ||||
|             const parsed_posts = []; | ||||
|             for (let post of posts) { | ||||
|                 parsed_posts.push(await parsePost(post, 1)); | ||||
|             } | ||||
|             profile_pinned_posts.set(parsed_posts); | ||||
|         }); | ||||
| 
 | ||||
|         let post_lock = false; // `true` == "locked" | ||||
|         getPosts(profile, null).then(last_id => { | ||||
|             profile_posts_max_id = last_id; | ||||
|             post_lock = false; | ||||
|         }); | ||||
| 
 | ||||
|         document.addEventListener("scroll", () => { | ||||
|             if (window.innerHeight + window.scrollY < document.body.offsetHeight - 2048) | ||||
|                 return; | ||||
|             if ($profile_posts.length == 0) | ||||
|                 return; | ||||
|             if (profile_posts_max_id == null) | ||||
|                 return; | ||||
|             if (profile_posts_max_id != $profile_posts[$profile_posts.length - 1].id) | ||||
|                 return; | ||||
| 
 | ||||
|             if (post_lock) return; | ||||
|             post_lock = true; | ||||
| 
 | ||||
|             getPosts(profile, profile_posts_max_id).then(last_id => { | ||||
|                 profile_posts_max_id = last_id; | ||||
|                 post_lock = false; | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         return profile; | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| {#await profile} | ||||
|     <div class="loading throb"> | ||||
|         <span>{lang.string('profile.loading')}</span> | ||||
|     </div> | ||||
| {:then profile} | ||||
|     <header data-banner="{profile.banner_url}"> | ||||
|         <img src="{profile.banner_url}" class="profile-banner" alt=""> | ||||
|         <div class="profile-tag"> | ||||
|             <!-- svelte-ignore a11y-img-redundant-alt --> | ||||
|             <img src="{profile.avatar_url}" alt=""> | ||||
|             <div class="profile-tag-names"> | ||||
|                 <div class="profile-tag-display-name"> | ||||
|                 <h1> | ||||
|                     {@html profile.rich_name} | ||||
|                     {#if profile.locked} | ||||
|                         <span title="{lang.string('profile.locked')}"> | ||||
|                             <LockIcon width="22px"/> | ||||
|                         </span> | ||||
|                     {/if} | ||||
|                     {#if profile.bot} | ||||
|                         <span title="{lang.string('profile.bot')}"> | ||||
|                             <BotIcon width="22px"/> | ||||
|                         </span> | ||||
|                     {/if} | ||||
|                 </h1> | ||||
|                 </div> | ||||
|                 <p>{profile.fqn}</p> | ||||
|             </div> | ||||
|         </div> | ||||
|     </header> | ||||
|     <div class="profile-info"> | ||||
|         <p class="profile-bio">{@html profile.rich_bio}</p> | ||||
|         <ul class="profile-counts"> | ||||
|             <li><b>{lang.string('profile.followers')}</b> {profile.followers_count}</li> | ||||
|             <li><b>{lang.string('profile.following')}</b> {profile.following_count}</li> | ||||
|             <li><b>{lang.string('profile.posts')}</b> {profile.posts_count}</li> | ||||
|         </ul> | ||||
|         <div class="profile-actions"> | ||||
|             {#if $account && profile.fqn !== $account.fqn} | ||||
|             <Button filled disabled label="{lang.string('profile.follow')} {profile.nickname}" class="profile-btn-follow"> | ||||
|                 {lang.string('profile.follow')} | ||||
|             </Button> | ||||
|             {/if} | ||||
|             <Button label="{lang.string('profile.home_instance')}" href="{profile.url}"> | ||||
|                 <HomeIcon width="24px"/> | ||||
|             </Button> | ||||
|             <Button> | ||||
|                 <MoreIcon width="24px"/> | ||||
|             </Button> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="profile-post-categories"> | ||||
|         <Button active> | ||||
|             {lang.string('profile.posts')} | ||||
|         </Button> | ||||
|         <Button> | ||||
|             {lang.string('profile.replies')} | ||||
|         </Button> | ||||
|         <Button> | ||||
|             {lang.string('profile.media')} | ||||
|         </Button> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="profile-pinned-posts"> | ||||
|         {#if profile_pinned_posts} | ||||
|             {#each $profile_pinned_posts as post} | ||||
|                 <Post post_data={post} pinned /> | ||||
|             {/each} | ||||
|             <br/><hr/><br/> | ||||
|         {/if} | ||||
|     </div> | ||||
|     <div class="profile-posts"> | ||||
|         {#each $profile_posts as post} | ||||
|             <Post post_data={post} /> | ||||
|         {/each} | ||||
|     </div> | ||||
| {:catch error} | ||||
|     <p class="error">{error}</p> | ||||
| {/await} | ||||
| 
 | ||||
| <style> | ||||
|     header { | ||||
|         margin-top: 1rem; | ||||
|         width: 100%; | ||||
|         height: 215px; | ||||
|         position: relative; | ||||
|     } | ||||
| 
 | ||||
|     .profile-banner { | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         object-fit: cover; | ||||
|         background-color: var(--bg-700); | ||||
|     } | ||||
| 
 | ||||
|     .profile-tag { | ||||
|         position: absolute; | ||||
|         bottom: 16px; | ||||
|         left: 16px; | ||||
|         background: color-mix(in srgb, transparent, var(--bg-1000) 50%); | ||||
|         backdrop-filter: blur(8px); | ||||
|         width: fit-content; | ||||
|         display: flex; | ||||
|         height: 64px; | ||||
|         border-radius: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .profile-tag img { | ||||
|         aspect-ratio: 1; | ||||
|         border-top-left-radius: 8px; | ||||
|         border-bottom-left-radius: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .profile-tag-names { | ||||
|         padding: 8px 16px; | ||||
|         align-self: center; | ||||
|     } | ||||
| 
 | ||||
|     .profile-tag-names * { | ||||
|         margin: 0; | ||||
|     } | ||||
| 
 | ||||
|     .profile-tag-names h1 { | ||||
|         font-size: 1.15rem | ||||
|     } | ||||
| 
 | ||||
|     .profile-tag-names h1 :global(svg) { | ||||
|         height: 1.2em; | ||||
|         width: 1.2em; | ||||
|         margin: -1em -.1em 0 -.1em; | ||||
|         transform: translateY(.2em); | ||||
|     } | ||||
| 
 | ||||
|     .profile-info { | ||||
|         background-color: var(--bg-800); | ||||
|         padding: 16px; | ||||
|         border-bottom-left-radius: 8px; | ||||
|         border-bottom-right-radius: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .profile-bio { | ||||
|         margin: 0; | ||||
| 
 | ||||
|         /* !! may not be required in prod */ | ||||
|         white-space: pre-line; | ||||
|     } | ||||
|     :global(.profile-bio p:first-of-type) { | ||||
|         margin: 0; | ||||
|     } | ||||
| 
 | ||||
|     .profile-counts { | ||||
|         padding: 0; | ||||
|     } | ||||
|     .profile-counts li { | ||||
|         display: inline-block; | ||||
|     } | ||||
|     .profile-counts > *:not(.profile-counts:first-child) { | ||||
|         margin-right: 16px; | ||||
|     } | ||||
| 
 | ||||
|     .profile-actions { | ||||
|         width: fit-content; | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: .5rem; | ||||
|     } | ||||
| 
 | ||||
|     .profile-actions :global(button.profile-btn-follow) { | ||||
|         padding: 0 32px; | ||||
|     } | ||||
| 
 | ||||
|     .profile-actions :global(a) { | ||||
|         width: fit-content; | ||||
|         height: 16px; | ||||
|     } | ||||
|     .profile-actions :global(button) { | ||||
|         width: fit-content; | ||||
|         height: 42px; | ||||
|     } | ||||
| 
 | ||||
|     .profile-post-categories { | ||||
|         display: flex; | ||||
|         padding: 1rem 0; | ||||
|         gap: 1rem; | ||||
|     } | ||||
| 
 | ||||
|     .loading { | ||||
|         width: 100%; | ||||
|         height: 80vh; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|         font-size: 2em; | ||||
|         font-weight: bold; | ||||
|     } | ||||
| 
 | ||||
|     .error { | ||||
|         padding: 4em 0; | ||||
|         width: 100%; | ||||
|         text-align: center; | ||||
|     } | ||||
| </style> | ||||
|  | @ -1,4 +1,8 @@ | |||
| export async function load({ params }) { | ||||
|     let handle = params.account; | ||||
|     if (handle.startsWith('@')) | ||||
|         handle = handle.substring(1); | ||||
| 
 | ||||
|     return { | ||||
|         server_host: params.server, | ||||
|         account_handle: params.account, | ||||
|  |  | |||
|  | @ -4,13 +4,16 @@ | |||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { parsePost } from '$lib/post.js'; | ||||
|     import { goto, afterNavigate } from '$app/navigation'; | ||||
|     import { base } from '$app/paths' | ||||
|     import { base } from '$app/paths'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     import Post from '$lib/ui/post/Post.svelte'; | ||||
|     import Button from '$lib/ui/Button.svelte'; | ||||
| 
 | ||||
|     export let data; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     let post = fetchPost(data.post_id); | ||||
|     let error = false; | ||||
|     let previous_page = base; | ||||
|  | @ -30,16 +33,16 @@ | |||
|             // TODO: make `server` a key/value pair to support multiple servers | ||||
|             server.set(await createServer(data.server_host)); | ||||
|             if (!$server) { | ||||
|                 error = `Failed to connect to <code>${data.server_host}</code>.`; | ||||
|                 console.error(`Failed to connect to ${data.server_host}.`); | ||||
|                 error = lang.string('error.connection_failed', data.server_host); | ||||
|                 console.error(lang.string('logs.connection_failed', data.server_host)); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const post_data = await api.getPost($server.host, token, post_id); | ||||
|         if (!post_data || post_data.error) { | ||||
|             error = `Failed to retrieve post <code>${post_id}</code>.`; | ||||
|             console.error(`Failed to retrieve post ${post_id}.`); | ||||
|             error = lang.string('error.post_fetch_failed_id', post_id); | ||||
|             console.error(lang.string('logs.post_fetch_failed_id', post_id)); | ||||
|             return; | ||||
|         } | ||||
|         let post = await parsePost(post_data, 0); | ||||
|  | @ -68,7 +71,7 @@ | |||
| 
 | ||||
| {#await post} | ||||
|     <div class="loading throb"> | ||||
|         <span>loading post...</span> | ||||
|         <span>{lang.string('post.loading')}</span> | ||||
|     </div> | ||||
| {:then post} | ||||
|     {#if error} | ||||
|  | @ -77,12 +80,12 @@ | |||
|         <header> | ||||
|             {#if previous_page} | ||||
|                 <nav> | ||||
|                     <Button centered on:click={() => {goto(previous_page)}}>Back</Button> | ||||
|                     <Button centered on:click={() => {goto(previous_page)}}>{lang.string('navigation.back')}</Button> | ||||
|                 </nav> | ||||
|             {/if} | ||||
|             <img src={post.account.avatar_url} type={post.account.avatar_type || "image/png"} alt="" width="40" height="40" class="header-avatar" loading="lazy" decoding="async"> | ||||
|             <img src={post.account.avatar_url} type={post.account.avatar_type || 'image/png'} alt="" width="40" height="40" class="header-avatar" loading="lazy" decoding="async"> | ||||
|             <h1> | ||||
|                 Post by {@html post.account.rich_name} | ||||
|                 {@html lang.string('post.by', post.account.rich_name)} | ||||
|             </h1> | ||||
|         </header> | ||||
|          | ||||
|  |  | |||
|  | @ -8,17 +8,20 @@ | |||
|     import { error } from '@sveltejs/kit'; | ||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||
|     import { account } from '$lib/stores/account.js'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     export let data; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     let auth_code = data.code; | ||||
| 
 | ||||
|     if (!auth_code || !get(server) || !get(app)) { | ||||
|         error(400, { message: "Bad request" }); | ||||
|         error(400, { message: lang.string('error.bad_request') }); | ||||
|     } else { | ||||
|         api.getToken(get(server).host, get(app).id, get(app).secret, auth_code).then(token => { | ||||
|             if (!token) { | ||||
|                 error(400, { message: "Invalid auth code provided" }); | ||||
|                 error(400, { message: lang.string('error.invalid_auth_code') }); | ||||
|             } | ||||
| 
 | ||||
|             app.update(app => { | ||||
|  | @ -30,7 +33,7 @@ | |||
|                 if (!data) return goto("/"); | ||||
| 
 | ||||
|                 account.set(parseAccount(data)); | ||||
|                 console.log(`Logged in as @${get(account).username}@${get(account).host}`); | ||||
|                 console.log(lang.string('logs.logged_in', get(account).fqn)); | ||||
| 
 | ||||
|                 // spin up async task to fetch notifications | ||||
|                 return api.getNotifications( | ||||
|  |  | |||
							
								
								
									
										36
									
								
								src/routes/favourites/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/routes/favourites/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| <script> | ||||
|     import { page } from '$app/stores'; | ||||
|     import { account } from '@cf/store/account.js'; | ||||
|     import { timeline, getTimeline } from '$lib/timeline.js'; | ||||
| 
 | ||||
|     import Button from '@cf/ui/Button.svelte'; | ||||
|     import Post from '@cf/ui/post/Post.svelte'; | ||||
|     import PageHeader from '@cf/ui/core/PageHeader.svelte'; | ||||
| 
 | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
|     if (!$account) goto("/"); | ||||
| 
 | ||||
|     getTimeline("favourites", true); | ||||
| 
 | ||||
|     document.addEventListener('scroll', () => { | ||||
|         if ($account && $page.url.pathname !== "/favourites") return; | ||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||
|             getTimeline("favourites"); | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| <PageHeader title={lang.string(`navigation.favourites`)}/> | ||||
| 
 | ||||
| <div id="feed" role="feed"> | ||||
|     {#if $timeline.length <= 0} | ||||
|         <div class="loading throb"> | ||||
|             <span>{lang.string('timeline.fetching')}</span> | ||||
|         </div> | ||||
|     {/if} | ||||
|     {#each $timeline as post} | ||||
|         <Post post_data={post} /> | ||||
|     {/each} | ||||
| </div> | ||||
							
								
								
									
										152
									
								
								src/routes/follow-requests/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/routes/follow-requests/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,152 @@ | |||
| <script> | ||||
|     import { followRequests } from '$lib/followRequests.js'; | ||||
|     import PageHeader from '../../lib/ui/core/PageHeader.svelte'; | ||||
|     import Lang from '$lib/lang'; | ||||
|     import {server} from '$lib/client/server'; | ||||
|     import {app} from '$lib/client/app'; | ||||
|     import Button from '../../lib/ui/Button.svelte'; | ||||
|     import * as api from '$lib/api' | ||||
| 
 | ||||
|     import TickIcon from '@cf/icons/tick.svg' | ||||
|     import CrossIcon from '@cf/icons/cross.svg' | ||||
|     import { get } from 'svelte/store'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     async function actionRequest(account_id, approved) { | ||||
|         // remove item from array first - this updates the ui and | ||||
|         // makes the interaction more seamless | ||||
|         $followRequests.splice( | ||||
|             $followRequests.indexOf( | ||||
|                 $followRequests.find(r => r.id) | ||||
|             ), | ||||
|             1 | ||||
|         ); | ||||
| 
 | ||||
|         // hack: force the state to update now that we just spliced the array | ||||
|         $followRequests = $followRequests | ||||
| 
 | ||||
|         if(approved) { | ||||
|             await api.acceptFollowRequest( | ||||
|                 get(server).host, | ||||
|                 get(app).token, | ||||
|                 account_id | ||||
|             ) | ||||
|         } else { | ||||
|             await api.rejectFollowRequest( | ||||
|                 get(server).host, | ||||
|                 get(app).token, | ||||
|                 account_id | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     // aliases | ||||
|     const acceptRequest = (id) => actionRequest(id, true); | ||||
|     const denyRequest = (id) => actionRequest(id, false); | ||||
| </script> | ||||
| 
 | ||||
| <PageHeader title={lang.string('navigation.follow_requests')}/> | ||||
| 
 | ||||
| {#if $followRequests.length < 1} | ||||
| <p class="request-zero">{lang.string('follow_requests.none')}</p> | ||||
| {/if} | ||||
| 
 | ||||
| <div class="requests-container"> | ||||
|     {#each $followRequests as req} | ||||
|         <div class="request"> | ||||
|             <a href="/{$server.host}/@{req.fqn}" class="request-avatar-container" on:mouseup|stopPropagation> | ||||
|                 <img src={req.avatar_url} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> | ||||
|             </a> | ||||
|             <div class="info"> | ||||
|                 <div class="request-user-info"> | ||||
|                     <a href="/{$server.host}/@{req.fqn}" class="name">{@html req.rich_name}</a> | ||||
|                     <span class="username">{req.mention}</span> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="request-options"> | ||||
|                 <Button filled title="Yes" on:click={() => acceptRequest(req.id)}> | ||||
|                     <TickIcon width="24px"/> | ||||
|                 </Button> | ||||
|                 <Button title="No" on:click={() => denyRequest(req.id)}> | ||||
|                     <CrossIcon width="24px"/> | ||||
|                 </Button> | ||||
|             </div> | ||||
|         </div> | ||||
|     {/each} | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     .request { | ||||
|         width: 100%; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
| 
 | ||||
|         background: var(--bg-900); | ||||
|         padding: .5rem; | ||||
|         border-radius: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .request a, | ||||
|     .request a:visited { | ||||
|         color: inherit; | ||||
|         text-decoration: none; | ||||
|     } | ||||
|     .request a:hover { | ||||
|         text-decoration: underline; | ||||
|     } | ||||
| 
 | ||||
|     .request-avatar-container { | ||||
|         margin-right: 12px; | ||||
|         display: flex; | ||||
|     } | ||||
| 
 | ||||
|     .post-avatar { | ||||
|         border-radius: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .info { | ||||
|         display: flex; | ||||
|         flex-grow: 1; | ||||
|         flex-direction: row; | ||||
|     } | ||||
| 
 | ||||
|     .request-user-info { | ||||
|         margin-top: -2px; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         justify-content: center; | ||||
|     } | ||||
| 
 | ||||
|     .request-user-info a { | ||||
|         display: block; | ||||
|     } | ||||
| 
 | ||||
|     .request-user-info .username { | ||||
|         opacity: .8; | ||||
|         font-size: .9em; | ||||
|     } | ||||
| 
 | ||||
|     .request-options { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|     } | ||||
| 
 | ||||
|     .request-options :global(button) { | ||||
|         width: fit-content; | ||||
|         height: 100%; | ||||
|     } | ||||
| 
 | ||||
|     .request-zero { | ||||
|         opacity: 0.8; | ||||
|         font-size: 0.95rem; | ||||
|         text-align: center; | ||||
|     } | ||||
| 
 | ||||
|     .requests-container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         gap: .5rem; | ||||
|     } | ||||
| </style> | ||||
|  | @ -4,6 +4,10 @@ | |||
|     import { goto } from '$app/navigation'; | ||||
|     import { page } from '$app/stores'; | ||||
|     import Notification from '$lib/ui/Notification.svelte'; | ||||
|     import PageHeader from '../../lib/ui/core/PageHeader.svelte'; | ||||
|     import Lang from '$lib/lang'; | ||||
| 
 | ||||
|     const lang = Lang(); | ||||
| 
 | ||||
|     if (!$account) goto("/"); | ||||
| 
 | ||||
|  | @ -30,14 +34,12 @@ | |||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| <header> | ||||
|     <h1>Notifications</h1> | ||||
| </header> | ||||
| <PageHeader title={lang.string('navigation.notifications')}/> | ||||
| 
 | ||||
| <div class="notifications"> | ||||
|     {#if $notifications.length === 0} | ||||
|         <div class="loading throb"> | ||||
|             <span>fetching notifications...</span> | ||||
|             <span>{lang.string('notification.fetching')}</span> | ||||
|         </div> | ||||
|     {:else} | ||||
|         {#each $notifications as notif} | ||||
|  | @ -47,18 +49,6 @@ | |||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     header { | ||||
|         width: 100%; | ||||
|         height: 64px; | ||||
|         margin: 16px 0 8px 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|     } | ||||
| 
 | ||||
|     h1 { | ||||
|         font-size: 1.5em; | ||||
|     } | ||||
| 
 | ||||
|     .loading { | ||||
|         width: 100%; | ||||
|         height: 80vh; | ||||
|  |  | |||
|  | @ -19,9 +19,10 @@ const config = { | |||
|             name: child_process.execSync('git rev-parse HEAD').toString().trim() | ||||
|         }, | ||||
|         alias: { | ||||
|             '@cf/ui/*': "./src/lib/ui", | ||||
|             '@cf/icons/*': "./src/img/icons", | ||||
|             '@cf/store/*': "./src/lib/stores" | ||||
|             '@cf/ui/*': './src/lib/ui', | ||||
|             '@cf/icons/*': './src/img/icons', | ||||
|             '@cf/store/*': './src/lib/stores', | ||||
|             '@cf/lang/*': './src/lang' | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
|  |  | |||
|  | @ -3,11 +3,16 @@ import { defineConfig } from 'vite'; | |||
| import { readFileSync } from 'fs'; | ||||
| import { fileURLToPath } from 'url'; | ||||
| import svg from '@poppanator/sveltekit-svg' | ||||
| import { execSync } from 'child_process'; | ||||
| 
 | ||||
| // get ver from package.json
 | ||||
| const packageFile = fileURLToPath(new URL('package.json', import.meta.url)); | ||||
| const packageData = readFileSync(packageFile, 'utf8'); | ||||
| const packageJSON = JSON.parse(packageData); | ||||
| 
 | ||||
| // get git commit hash
 | ||||
| const commitHash = execSync("git rev-parse HEAD") | ||||
|     .toString().trim().slice(0, 10) | ||||
| 
 | ||||
| export default defineConfig({ | ||||
| 	plugins: [ | ||||
|  | @ -15,7 +20,8 @@ export default defineConfig({ | |||
|         svg() | ||||
|     ], | ||||
|     define: { | ||||
|         APP_VERSION: JSON.stringify(packageJSON.version) | ||||
|         APP_VERSION: JSON.stringify(packageJSON.version), | ||||
|         APP_COMMIT: `"${commitHash}"` | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue