diff --git a/CHANGELOGS.md b/CHANGELOGS.md index 4c81497..d5cee41 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -1,3 +1,16 @@ +# Campfire v0.4.0 +- Huge refactor, along with some improved documentation +- Custom emotes now show in the sidebar profile display +- Infinite scrolling notifications +- Notifications now show content warnings where applicable +- Notifications now show custom emoji +- Notifications now show post media +- Boosts now reflect the visibility of the original post +- Added compose box, and the ability to create posts +- Added button to delete own posts +- Rewrote Campfire URLs so they can be viewed anonymously +- Improved UI tweaks + # Campfire v0.3.0 - Added notifications view - Many more background tweaks, fixes, and optimisations diff --git a/package-lock.json b/package-lock.json index 68c3b16..ef4c2d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "campfire-client", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "campfire-client", - "version": "0.3.0", + "version": "0.4.0", "license": "GPL-3.0", "devDependencies": { "@poppanator/sveltekit-svg": "^4.2.1", "@sveltejs/adapter-auto": "^3.2.2", "@sveltejs/adapter-static": "^3.0.2", - "@sveltejs/kit": "^2.5.17", + "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^3.1.1", "svelte": "^4.2.18", "vite": "^5.3.1" @@ -477,9 +477,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.25", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", - "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", "dev": true, "license": "MIT" }, @@ -526,9 +526,9 @@ "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz", + "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==", "cpu": [ "arm" ], @@ -540,9 +540,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz", + "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==", "cpu": [ "arm64" ], @@ -554,9 +554,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz", + "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==", "cpu": [ "arm64" ], @@ -568,9 +568,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz", + "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==", "cpu": [ "x64" ], @@ -581,10 +581,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz", + "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz", + "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz", + "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==", "cpu": [ "arm" ], @@ -596,9 +624,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz", + "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==", "cpu": [ "arm" ], @@ -610,9 +638,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz", + "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==", "cpu": [ "arm64" ], @@ -624,9 +652,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz", + "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==", "cpu": [ "arm64" ], @@ -637,10 +665,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz", + "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz", + "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==", "cpu": [ "ppc64" ], @@ -652,9 +694,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz", + "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==", "cpu": [ "riscv64" ], @@ -666,9 +708,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz", + "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==", "cpu": [ "s390x" ], @@ -680,9 +722,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz", + "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==", "cpu": [ "x64" ], @@ -694,9 +736,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz", + "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==", "cpu": [ "x64" ], @@ -708,9 +750,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz", + "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==", "cpu": [ "arm64" ], @@ -722,9 +764,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz", + "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==", "cpu": [ "ia32" ], @@ -736,9 +778,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz", + "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==", "cpu": [ "x64" ], @@ -773,25 +815,23 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.17", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz", - "integrity": "sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.16.0.tgz", + "integrity": "sha512-S9i1ZWKqluzoaJ6riYnEdbe+xJluMTMkhABouBa66GaWcAyCjW/jAc0NdJQJ/DXyK1CnP5quBW25e99MNyvLxA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", - "devalue": "^5.0.0", - "esm-env": "^1.0.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", - "sirv": "^2.0.4", - "tiny-glob": "^0.2.9" + "sirv": "^3.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" @@ -800,9 +840,9 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3" + "vite": "^5.0.3 || ^6.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte": { @@ -864,9 +904,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -1063,9 +1103,9 @@ } }, "node_modules/devalue": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.0.0.tgz", - "integrity": "sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", "dev": true, "license": "MIT" }, @@ -1181,9 +1221,9 @@ } }, "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "dev": true, "license": "MIT" }, @@ -1212,20 +1252,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, "node_modules/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", @@ -1309,9 +1335,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -1353,9 +1379,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -1372,9 +1398,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "dev": true, "funding": [ { @@ -1392,22 +1418,22 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz", + "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -1417,22 +1443,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", + "@rollup/rollup-android-arm-eabi": "4.31.0", + "@rollup/rollup-android-arm64": "4.31.0", + "@rollup/rollup-darwin-arm64": "4.31.0", + "@rollup/rollup-darwin-x64": "4.31.0", + "@rollup/rollup-freebsd-arm64": "4.31.0", + "@rollup/rollup-freebsd-x64": "4.31.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.31.0", + "@rollup/rollup-linux-arm-musleabihf": "4.31.0", + "@rollup/rollup-linux-arm64-gnu": "4.31.0", + "@rollup/rollup-linux-arm64-musl": "4.31.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.31.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0", + "@rollup/rollup-linux-riscv64-gnu": "4.31.0", + "@rollup/rollup-linux-s390x-gnu": "4.31.0", + "@rollup/rollup-linux-x64-gnu": "4.31.0", + "@rollup/rollup-linux-x64-musl": "4.31.0", + "@rollup/rollup-win32-arm64-msvc": "4.31.0", + "@rollup/rollup-win32-ia32-msvc": "4.31.0", + "@rollup/rollup-win32-x64-msvc": "4.31.0", "fsevents": "~2.3.2" } }, @@ -1457,9 +1486,9 @@ "license": "MIT" }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", "dev": true, "license": "MIT", "dependencies": { @@ -1468,13 +1497,13 @@ "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1482,9 +1511,9 @@ } }, "node_modules/svelte": { - "version": "4.2.18", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", - "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", "dev": true, "license": "MIT", "dependencies": { @@ -1546,17 +1575,6 @@ "url": "https://opencollective.com/svgo" } }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -1568,15 +1586,15 @@ } }, "node_modules/vite": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", - "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -1595,6 +1613,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -1612,6 +1631,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, diff --git a/package.json b/package.json index 301641e..e039bc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "campfire-client", - "version": "0.3.0", + "version": "0.4.0", "description": "social media for the galaxy-wide-web! 🌌", "private": true, "type": "module", @@ -18,7 +18,7 @@ "@poppanator/sveltekit-svg": "^4.2.1", "@sveltejs/adapter-auto": "^3.2.2", "@sveltejs/adapter-static": "^3.0.2", - "@sveltejs/kit": "^2.5.17", + "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^3.1.1", "svelte": "^4.2.18", "vite": "^5.3.1" diff --git a/src/img/icons/bin.svg b/src/img/icons/bin.svg new file mode 100644 index 0000000..5476ee2 --- /dev/null +++ b/src/img/icons/bin.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/img/icons/bookmark.svg b/src/img/icons/bookmark.svg index b9e9b8e..70f328d 100644 --- a/src/img/icons/bookmark.svg +++ b/src/img/icons/bookmark.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/img/icons/dm.svg b/src/img/icons/dm.svg new file mode 100644 index 0000000..41b64da --- /dev/null +++ b/src/img/icons/dm.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/img/icons/error.svg b/src/img/icons/error.svg new file mode 100644 index 0000000..cdcc032 --- /dev/null +++ b/src/img/icons/error.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/img/icons/explore.svg b/src/img/icons/explore.svg index 9699f07..e2a289c 100644 --- a/src/img/icons/explore.svg +++ b/src/img/icons/explore.svg @@ -1,10 +1,10 @@ - + - + - + diff --git a/src/img/icons/followers.svg b/src/img/icons/followers.svg new file mode 100644 index 0000000..7293c31 --- /dev/null +++ b/src/img/icons/followers.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/img/icons/hashtag.svg b/src/img/icons/hashtag.svg index 5a574d7..52f7097 100644 --- a/src/img/icons/hashtag.svg +++ b/src/img/icons/hashtag.svg @@ -1,10 +1,10 @@ - - - - - - - - - + + + + + + + + + diff --git a/src/img/icons/info.svg b/src/img/icons/info.svg index 23077aa..e9342e4 100644 --- a/src/img/icons/info.svg +++ b/src/img/icons/info.svg @@ -1,5 +1,3 @@ - - - - + + diff --git a/src/img/icons/like.svg b/src/img/icons/like.svg index a2ffd55..45682c9 100644 --- a/src/img/icons/like.svg +++ b/src/img/icons/like.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/img/icons/like_fill.svg b/src/img/icons/like_fill.svg index 6d2eb3b..09aa5b8 100644 --- a/src/img/icons/like_fill.svg +++ b/src/img/icons/like_fill.svg @@ -1,10 +1,10 @@ - + - + - + diff --git a/src/img/icons/lists.svg b/src/img/icons/lists.svg index a3e5c30..47a99bb 100644 --- a/src/img/icons/lists.svg +++ b/src/img/icons/lists.svg @@ -1,3 +1,10 @@ - - + + + + + + + + + diff --git a/src/img/icons/logout.svg b/src/img/icons/logout.svg index 1a3cf80..d97e04d 100644 --- a/src/img/icons/logout.svg +++ b/src/img/icons/logout.svg @@ -1,11 +1,11 @@ - + - - + + - + diff --git a/src/img/icons/media.svg b/src/img/icons/media.svg new file mode 100644 index 0000000..f88b45e --- /dev/null +++ b/src/img/icons/media.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/img/icons/mention.svg b/src/img/icons/mention.svg new file mode 100644 index 0000000..3895acb --- /dev/null +++ b/src/img/icons/mention.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/img/icons/more.svg b/src/img/icons/more.svg index bc531b2..ab0f996 100644 --- a/src/img/icons/more.svg +++ b/src/img/icons/more.svg @@ -1,5 +1,3 @@ - - - - + + diff --git a/src/img/icons/notifications.svg b/src/img/icons/notifications.svg index e946ca5..7e3dfa6 100644 --- a/src/img/icons/notifications.svg +++ b/src/img/icons/notifications.svg @@ -1,11 +1,11 @@ - + - - + + - + diff --git a/src/img/icons/plus.svg b/src/img/icons/plus.svg new file mode 100644 index 0000000..0bcf501 --- /dev/null +++ b/src/img/icons/plus.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/img/icons/plus_fill.svg b/src/img/icons/plus_fill.svg new file mode 100644 index 0000000..cd16e2a --- /dev/null +++ b/src/img/icons/plus_fill.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/img/icons/poll.svg b/src/img/icons/poll.svg new file mode 100644 index 0000000..f258b9d --- /dev/null +++ b/src/img/icons/poll.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/img/icons/post.svg b/src/img/icons/post.svg index 79d5536..3d4ac43 100644 --- a/src/img/icons/post.svg +++ b/src/img/icons/post.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/img/icons/public.svg b/src/img/icons/public.svg new file mode 100644 index 0000000..c321adf --- /dev/null +++ b/src/img/icons/public.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/img/icons/quote.svg b/src/img/icons/quote.svg index 98ce541..5a74811 100644 --- a/src/img/icons/quote.svg +++ b/src/img/icons/quote.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/src/img/icons/react.svg b/src/img/icons/react.svg index 532213d..e7396e9 100644 --- a/src/img/icons/react.svg +++ b/src/img/icons/react.svg @@ -1,15 +1,14 @@ - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + diff --git a/src/img/icons/reload.svg b/src/img/icons/reload.svg new file mode 100644 index 0000000..4e58201 --- /dev/null +++ b/src/img/icons/reload.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/img/icons/reply.svg b/src/img/icons/reply.svg index 5462c7b..dc18cb0 100644 --- a/src/img/icons/reply.svg +++ b/src/img/icons/reply.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/img/icons/repost.svg b/src/img/icons/repost.svg index c0f950f..b679f16 100644 --- a/src/img/icons/repost.svg +++ b/src/img/icons/repost.svg @@ -1,12 +1,12 @@ - + - - - + + + - + diff --git a/src/img/icons/search.svg b/src/img/icons/search.svg index 0864af4..eecb00c 100644 --- a/src/img/icons/search.svg +++ b/src/img/icons/search.svg @@ -1,11 +1,10 @@ - + - - + - + diff --git a/src/img/icons/settings.svg b/src/img/icons/settings.svg index be2926e..bfc8697 100644 --- a/src/img/icons/settings.svg +++ b/src/img/icons/settings.svg @@ -1,11 +1,11 @@ - + - + - + - + diff --git a/src/img/icons/timeline.svg b/src/img/icons/timeline.svg index a2be615..712e88f 100644 --- a/src/img/icons/timeline.svg +++ b/src/img/icons/timeline.svg @@ -1,12 +1,10 @@ - + - - - + - + diff --git a/src/img/icons/unlisted.svg b/src/img/icons/unlisted.svg new file mode 100644 index 0000000..6b88c40 --- /dev/null +++ b/src/img/icons/unlisted.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/img/icons/warning.svg b/src/img/icons/warning.svg new file mode 100644 index 0000000..0fffc6f --- /dev/null +++ b/src/img/icons/warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/account.js b/src/lib/account.js new file mode 100644 index 0000000..7bfd696 --- /dev/null +++ b/src/lib/account.js @@ -0,0 +1,52 @@ +import { server } from '$lib/client/server.js'; +import { parseEmoji, renderEmoji } from '$lib/emoji.js'; +import { get, writable } from 'svelte/store'; + +const cache = writable({}); + +/** + * Parses an account using API data, and returns a writable store object. + * @param {Object} data + * @param {number} ancestor_count + */ +export function parseAccount(data) { + if (!data) { + console.error("Attempted to parse account data but no data was provided"); + return null; + } + let account = get(cache)[data.id]; + if (account) return account; + // cache miss! + + account = {}; + account.id = data.id; + account.nickname = data.display_name.trim(); + account.username = data.username; + account.name = account.nickname || account.username; + account.avatar_url = data.avatar; + account.url = data.url; + + if (data.acct.includes('@')) + account.host = data.acct.split('@')[1]; + else + account.host = get(server).host; + + account.mention = "@" + account.username; + if (account.host != get(server).host) + account.mention += "@" + account.host; + + account.emojis = {}; + data.emojis.forEach(emoji => { + account.emojis[emoji.shortcode] = parseEmoji(emoji.shortcode, emoji.url); + }); + + account.rich_name = account.nickname ? renderEmoji(account.nickname, account.emojis) : account.username; + + cache.update(cache => { + cache[account.id] = account; + return cache; + }); + + return account; +} + diff --git a/src/lib/api.js b/src/lib/api.js new file mode 100644 index 0000000..61f6bd9 --- /dev/null +++ b/src/lib/api.js @@ -0,0 +1,423 @@ +/** + * GET /api/v1/instance + * @param {string} host - The domain of the target server. + */ +export async function getInstance(host) { + const data = await fetch(`https://${host}/api/v1/instance`) + .then(res => res.json()) + .catch(error => console.error(error)); + return data ? data : false; +} + +/** + * POST /api/v1/apps + * Attempts to create an application for a given server host. + * @param {string} host - The domain of the target server. + */ +export async function createApp(host) { + let form = new FormData(); + form.append("client_name", "Campfire"); + form.append("redirect_uris", `${location.origin}/callback`); + form.append("scopes", "read write push"); + form.append("website", "https://campfire.bliss.town"); + + const res = await fetch(`https://${host}/api/v1/apps`, { + method: "POST", + body: form, + }) + .then(res => res.json()) + .catch(error => { + console.error(error); + return false; + }); + + if (!res || !res.client_id) return false; + + return { + id: res.client_id, + secret: res.client_secret, + }; +} + +/** + * Returns the OAuth authorization url for the target server. + * @param {string} host - The domain of the target server. + * @param {string} app_id - The application id for the target server. + */ +export function getOAuthUrl(host, app_id) { + return `https://${host}/oauth/authorize` + + `?client_id=${app_id}` + + "&scope=read+write+push" + + `&redirect_uri=${location.origin}/callback` + + "&response_type=code"; +} + +/** + * POST /oauth/token + * Attempts to generate an OAuth token. + * Returns false on failure. + * @param {string} host - The domain of the target server. + * @param {string} client_id - The application id. + * @param {string} secret - The application secret. + * @param {string} code - The authorization code provided by OAuth. + */ +export async function getToken(host, client_id, secret, code) { + let form = new FormData(); + form.append("client_id", client_id); + form.append("client_secret", secret); + form.append("redirect_uri", `${location.origin}/callback`); + form.append("grant_type", "authorization_code"); + form.append("code", code); + form.append("scope", "read write push"); + + const res = await fetch(`https://${host}/oauth/token`, { + method: "POST", + body: form, + }) + .then(res => res.json()) + .catch(error => { + console.error(error); + return false; + }); + + if (!res || !res.access_token) return false; + + return res.access_token; +} + +/** + * POST /oauth/revoke + * Attempts to revoke an OAuth token. + * Returns false on failure. + * @param {string} host - The domain of the target server. + * @param {string} client_id - The application id. + * @param {string} secret - The application secret. + * @param {string} token - The application token. + */ +export async function revokeToken(host, client_id, secret, token) { + let form = new FormData(); + form.append("client_id", client_id); + form.append("client_secret", secret); + form.append("token", token); + + const res = await fetch(`https://${host}/oauth/revoke`, { + method: "POST", + body: form, + }) + .catch(error => { + console.error(error); + return false; + }); + + if (!res.ok) return false; + return true; +} + +/** + * GET /api/v1/accounts/verify_credentials + * This endpoint returns information about the client account, + * and other useful data. + * Returns false on failure. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + */ +export async function verifyCredentials(host, token) { + let url = `https://${host}/api/v1/accounts/verify_credentials`; + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": "Bearer " + token } + }).then(res => res.json()); + + return data; +} + +/** + * GET /api/v1/streaming/health + * Checks if the server's streaming service is alive + */ +export async function getStreamingHealth(host) { + let url = `https://${host}/api/v1/streaming/health`; + const res = await fetch(url, { + method: 'GET' + }); + + return res.ok; +} + +/** + * GET /api/v1/notifications + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} min_id - If provided, only shows notifications after this ID. + * @param {string} max_id - If provided, only shows notifications before this ID. + * @param {string} limit - The maximum number of notifications to retrieve (default 40). + * @param {string} types - A list of notification types to filter to. + */ +export async function getNotifications(host, token, min_id, max_id, limit, types) { + let url = `https://${host}/api/v1/notifications`; + + let params = new URLSearchParams(); + if (min_id) params.append("min_id", min_id); + if (max_id) params.append("max_id", max_id); + if (limit) params.append("limit", limit); + if (types) params.append("types", types.join(',')); + 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; +} + +/** + * 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. + */ +export async function getTimeline(host, token, timeline, max_id) { + let url = `https://${host}/api/v1/timelines/${timeline || "home"}`; + + let params = new URLSearchParams(); + if (max_id) params.append("max_id", max_id); + const params_string = params.toString(); + if (params_string) url += '?' + params_string; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()); + + return data; +} + +/** + * GET /api/v1/statuses/{post_id}. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to fetch. + */ +export async function getPost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}`; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()) + + if (!data || data.error) return false; + + return data; +} + +/** + * POST /api/v1/statuses + * @param {string} host - The domain of the target server. + * @param {string} token - The application token + * @param {any} post_data - The post content + */ +export async function createPost(host, token, post_data) { + let formdata = new FormData(); + for (const key in post_data) { + formdata.append(key, post_data[key]); + } + + let url = `https://${host}/api/v1/statuses`; + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` }, + body: formdata + }) + + return await data.json(); +} + +/** + * PUT /api/v1/statuses/{post_id} + * @param {string} host - The domain of the target server. + * @param {string} token - The application token + * @param {any} post_id - The ID of the post to edit. + * @param {any} post_data - The post content + */ +export async function editPost(host, token, post_id, post_data) { + let formdata = new FormData(); + for (const key in post_data) { + formdata.append(key, post_data[key]); + } + + let url = `https://${host}/api/v1/statuses/${post_id}`; + const data = await fetch(url, { + method: 'PUT', + headers: { "Authorization": `Bearer ${token}` }, + body: formdata + }) + + return await data.json(); +} + +/** + * DELETE /api/v1/statuses/{post_id} + * Returns the deleted post's data, in the case of republishing. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token + * @param {any} post_id - The ID of the post to delete. + */ +export async function deletePost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}`; + const data = await fetch(url, { + method: 'DELETE', + headers: { "Authorization": `Bearer ${token}` }, + }) + + return await data.json(); +} + +/** + * GET /api/v1/statuses/{post_id}/context. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to fetch. + */ +export async function getPostContext(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/context`; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/reblog. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to boost. + * @param {string} visibility - The visibility with which to boost the post. + */ +export async function boostPost(host, token, post_id, visibility) { + let url = `https://${host}/api/v1/statuses/${post_id}/reblog`; + + let form = new FormData(); + if (visibility) form.append("visibility", visibility); + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` }, + body: form, + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/unreblog. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to unboost. + */ +export async function unboostPost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/unreblog`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/favourite. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to favourite. + */ +export async function favouritePost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/favourite`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/unfavourite. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to unfavourite. + */ +export async function unfavouritePost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/unfavourite`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/react/{shortcode} + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to favourite. + * @param {string} shortcode - The shortcode of the emote to react with. + */ +export async function reactPost(host, token, post_id, shortcode) { + // note: reacting with foreign emotes is unsupported on most servers + // chuckya appears to allow this, but other servers tested have + // not demonstrated this. + let url = `https://${host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/unreact/{shortcode} + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to favourite. + * @param {string} shortcode - The shortcode of the reaction emote to remove. + */ +export async function unreactPost(host, token, post_id, shortcode) { + let url = `https://${host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * GET /api/v1/accounts/{user_id} + * @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 getUser(host, token, user_id) { + let url = `https://${host}/api/v1/accounts/${user_id}`; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()); + + return data; +} diff --git a/src/lib/app.css b/src/lib/app.css index d010639..d6656b4 100644 --- a/src/lib/app.css +++ b/src/lib/app.css @@ -74,6 +74,11 @@ main { width: 732px; } +img.emoji { + height: 1.2em; + margin: -.2em 0; +} + .throb { animation: .25s throb alternate infinite ease-in; } diff --git a/src/lib/client/api.js b/src/lib/client/api.js deleted file mode 100644 index 09e6514..0000000 --- a/src/lib/client/api.js +++ /dev/null @@ -1,339 +0,0 @@ -import { client } from '$lib/client/client.js'; -import { user } from '$lib/stores/user.js'; -import { capabilities } from '../client/instance.js'; -import Post from '$lib/post.js'; -import User from '$lib/user/user.js'; -import Emoji from '$lib/emoji.js'; -import { get } from 'svelte/store'; - -export async function createApp(host) { - let form = new FormData(); - form.append("client_name", "Campfire"); - form.append("redirect_uris", `${location.origin}/callback`); - form.append("scopes", "read write push"); - form.append("website", "https://campfire.bliss.town"); - - const res = await fetch(`https://${host}/api/v1/apps`, { - method: "POST", - body: form, - }) - .then(res => res.json()) - .catch(error => { - console.error(error); - return false; - }); - - if (!res || !res.client_id) return false; - - return { - id: res.client_id, - secret: res.client_secret, - }; -} - -export function getOAuthUrl() { - return `https://${get(client).instance.host}/oauth/authorize` + - `?client_id=${get(client).app.id}` + - "&scope=read+write+push" + - `&redirect_uri=${location.origin}/callback` + - "&response_type=code"; -} - -export async function getToken(code) { - let form = new FormData(); - form.append("client_id", get(client).app.id); - form.append("client_secret", get(client).app.secret); - form.append("redirect_uri", `${location.origin}/callback`); - form.append("grant_type", "authorization_code"); - form.append("code", code); - form.append("scope", "read write push"); - - const res = await fetch(`https://${get(client).instance.host}/oauth/token`, { - method: "POST", - body: form, - }) - .then(res => res.json()) - .catch(error => { - console.error(error); - return false; - }); - - if (!res || !res.access_token) return false; - - return res.access_token; -} - -export async function revokeToken() { - let form = new FormData(); - form.append("client_id", get(client).app.id); - form.append("client_secret", get(client).app.secret); - form.append("token", get(client).app.token); - - const res = await fetch(`https://${get(client).instance.host}/oauth/revoke`, { - method: "POST", - body: form, - }) - .catch(error => { - console.error(error); - return false; - }); - - if (!res.ok) return false; - return true; -} - -export async function verifyCredentials() { - let url = `https://${get(client).instance.host}/api/v1/accounts/verify_credentials`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => res.json()); - - return data; -} - -export async function getNotifications(since_id, limit, types) { - if (!get(user)) return false; - - let url = `https://${get(client).instance.host}/api/v1/notifications`; - - let params = new URLSearchParams(); - if (since_id) params.append("since_id", since_id); - if (limit) params.append("limit", limit); - if (types) params.append("types", types.join(',')); - const params_string = params.toString(); - if (params_string) url += '?' + params_string; - - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => res.json()); - - return data; -} - -export async function getTimeline(last_post_id) { - if (!get(user)) return false; - let url = `https://${get(client).instance.host}/api/v1/timelines/home`; - if (last_post_id) url += "?max_id=" + last_post_id; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => res.json()); - - return data; -} - -export async function getPost(post_id, ancestor_count) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function getPostContext(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/context`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function boostPost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/reblog`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function unboostPost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreblog`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function favouritePost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/favourite`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function unfavouritePost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unfavourite`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function reactPost(post_id, shortcode) { - // for whatever reason (at least in my testing on iceshrimp) - // using shortcodes for external emoji results in a fallback - // to the default like emote. - // identical api calls on chuckya instances do not display - // this behaviour. - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function unreactPost(post_id, shortcode) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function parsePost(data, ancestor_count) { - let post = new Post(); - - post.text = data.content; - post.html = data.content; - - post.reply = null; - if ((data.in_reply_to_id || data.reply) && - ancestor_count !== 0 - ) { - const reply_data = data.reply || await getPost(data.in_reply_to_id, ancestor_count - 1); - // if the post returns false, we probably don't have permission to read it. - // we'll respect the thread's privacy, and leave it alone :) - if (!reply_data) return false; - post.reply = await parsePost(reply_data, ancestor_count - 1, false); - } - - post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; - - post.id = data.id; - post.created_at = new Date(data.created_at); - post.user = await parseUser(data.account); - post.warning = data.spoiler_text; - post.boost_count = data.reblogs_count; - post.reply_count = data.replies_count; - post.favourite_count = data.favourites_count; - post.favourited = data.favourited; - post.boosted = data.reblogged; - post.mentions = data.mentions; - post.files = data.media_attachments; - post.url = data.url; - post.visibility = data.visibility; - - post.emojis = []; - if (data.emojis) { - data.emojis.forEach(emoji_data => { - let name = emoji_data.shortcode.split('@')[0]; - post.emojis.push(parseEmoji({ - id: name + '@' + post.user.host, - name: name, - host: post.user.host, - url: emoji_data.url, - })); - }); - } - - if (data.reactions && get(client).instance.capabilities.includes(capabilities.REACTIONS)) { - post.reactions = parseReactions(data.reactions); - } - return post; -} - -export async function parseUser(data) { - if (!data) { - console.error("Attempted to parse user data but no data was provided"); - return null; - } - let user = await get(client).getCacheUser(data.id); - - if (user) return user; - // cache miss! - - user = new User(); - user.id = data.id; - user.nickname = data.display_name.trim(); - user.username = data.username; - user.avatar_url = data.avatar; - user.url = data.url; - - if (data.acct.includes('@')) - user.host = data.acct.split('@')[1]; - else - user.host = get(client).instance.host; - - user.emojis = []; - data.emojis.forEach(emoji_data => { - emoji_data.id = emoji_data.shortcode + '@' + user.host; - emoji_data.name = emoji_data.shortcode; - emoji_data.host = user.host; - user.emojis.push(parseEmoji(emoji_data)); - }); - - get(client).putCacheUser(user); - return user; -} - -export function parseReactions(data) { - let reactions = []; - data.forEach(reaction_data => { - let reaction = { - count: reaction_data.count, - name: reaction_data.name, - me: reaction_data.me, - }; - if (reaction_data.url) reaction.url = reaction_data.url; - reactions.push(reaction); - }); - return reactions; -} - -export function parseEmoji(data) { - let emoji = new Emoji( - data.id, - data.name, - data.host, - data.url, - ); - get(client).putCacheEmoji(emoji); - return emoji; -} - -export async function getUser(user_id) { - let url = `https://${get(client).instance.host}/api/v1/accounts/${user_id}`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => res.json()); - - return data; -} diff --git a/src/lib/client/app.js b/src/lib/client/app.js new file mode 100644 index 0000000..e17ead4 --- /dev/null +++ b/src/lib/client/app.js @@ -0,0 +1,37 @@ +import { writable } from 'svelte/store'; +import { app_name } from '$lib/config.js'; +import { browser } from "$app/environment"; + +// if app is falsy, assume user has not begun the login process. +// if app.token is falsy, assume user has not logged in. +export const app = writable(loadApp()); + +// write to localStorage on each update +app.subscribe(app => { + saveApp(app); +}); + +/** + * Saves the provided app to localStorage. + * If `app` is falsy, data is removed from localStorage. + * @param {Object} app + */ +function saveApp(app) { + if (!browser) return; + if (!app) { + localStorage.removeItem(app_name + "_app"); + return; + } + localStorage.setItem(app_name + "_app", JSON.stringify(app)); +} + +/** + * Returns application data loaded from localStorage, if it exists. + * Otherwise, returns false. + */ +function loadApp() { + if (!browser) return; + let data = localStorage.getItem(app_name + "_app"); + if (!data) return false; + return JSON.parse(data); +} diff --git a/src/lib/client/client.js b/src/lib/client/client.js deleted file mode 100644 index b1beb6e..0000000 --- a/src/lib/client/client.js +++ /dev/null @@ -1,192 +0,0 @@ -import { Instance, server_types } from './instance.js'; -import * as api from './api.js'; -import { get, writable } from 'svelte/store'; -import { last_read_notif_id } from '$lib/notifications.js'; -import { user, logged_in } from '$lib/stores/user.js'; - -export const client = writable(false); - -const save_name = "campfire"; - -export class Client { - instance; - app; - #cache; - - constructor() { - this.instance = null; - this.app = null; - this.cache = { - users: {}, - emojis: {}, - }; - } - - async init(host) { - if (host.startsWith("https://")) host = host.substring(8); - const url = `https://${host}/api/v1/instance`; - const data = await fetch(url).then(res => res.json()).catch(error => { console.error(error) }); - if (!data) { - console.error(`Failed to connect to ${host}`); - return `Failed to connect to ${host}!`; - } - - this.instance = new Instance(host, data.version); - if (this.instance.type == server_types.UNSUPPORTED) { - console.warn(`Server ${host} is unsupported - ${data.version}`); - if (!confirm( - `This app does not officially support ${host}. ` + - `Things may break, or otherwise not work as epxected! ` + - `Are you sure you wish to continue?` - )) return false; - } else { - console.log(`Server is "${this.instance.type}" (or compatible) with capabilities: [${this.instance.capabilities}].`); - } - - this.app = await api.createApp(host); - - if (!this.app || !this.instance) { - console.error("Failed to create app. Check the network logs for details."); - return false; - } - - this.save(); - - client.set(this); - - return true; - } - - getOAuthUrl() { - return api.getOAuthUrl(this.app.secret); - } - - async getToken(code) { - const token = await api.getToken(code); - if (!token) { - console.error("Failed to obtain access token"); - return false; - } - return token; - } - - async revokeToken() { - return await api.revokeToken(); - } - - async getNotifications(since_id, limit, types) { - return await api.getNotifications(since_id, limit, types); - } - - async getTimeline(last_post_id) { - return await api.getTimeline(last_post_id); - } - - async getPost(post_id, parent_replies, child_replies) { - return await api.getPost(post_id, parent_replies, child_replies); - } - - async getPostContext(post_id) { - return await api.getPostContext(post_id); - } - - async boostPost(post_id) { - return await api.boostPost(post_id); - } - - async unboostPost(post_id) { - return await api.unboostPost(post_id); - } - - async favouritePost(post_id) { - return await api.favouritePost(post_id); - } - - async unfavouritePost(post_id) { - return await api.unfavouritePost(post_id); - } - - async reactPost(post_id, shortcode) { - return await api.reactPost(post_id, shortcode); - } - - async unreactPost(post_id, shortcode) { - return await api.unreactPost(post_id, shortcode); - } - - putCacheUser(user) { - this.cache.users[user.id] = user; - client.set(this); - } - - async getCacheUser(user_id) { - let user = this.cache.users[user_id]; - if (user) return user; - - return false; - } - - async getUserByMention(mention) { - let users = Object.values(this.cache.users); - for (let i in users) { - const user = users[i]; - if (user.mention == mention) return user; - } - return false; - } - - putCacheEmoji(emoji) { - this.cache.emojis[emoji.id] = emoji; - client.set(this); - } - - getEmoji(emoji_id) { - let emoji = this.cache.emojis[emoji_id]; - if (!emoji) return false; - return emoji; - } - - async getUser(user_id) { - return await api.getUser(user_id); - } - - save() { - if (typeof localStorage === typeof undefined) return; - localStorage.setItem(save_name, JSON.stringify({ - version: APP_VERSION, - instance: { - host: this.instance.host, - version: this.instance.version, - }, - last_read_notif_id: get(last_read_notif_id), - app: this.app, - })); - } - - load() { - if (typeof localStorage === typeof undefined) return; - let json = localStorage.getItem(save_name); - if (!json) return false; - let saved = JSON.parse(json); - if (!saved.version || saved.version !== APP_VERSION) { - localStorage.removeItem(save_name); - return false; - } - this.instance = new Instance(saved.instance.host, saved.instance.version); - last_read_notif_id.set(saved.last_read_notif_id || 0); - this.app = saved.app; - client.set(this); - return true; - } - - async logout() { - if (!this.instance || !this.app) return; - if (!await this.revokeToken()) { - console.warn("Failed to log out correctly; ditching the old tokens anyways."); - } - localStorage.removeItem(save_name); - logged_in.set(false); - client.set(new Client()); - console.log("Logged out successfully."); - } -} diff --git a/src/lib/client/instance.js b/src/lib/client/instance.js deleted file mode 100644 index 92003e8..0000000 --- a/src/lib/client/instance.js +++ /dev/null @@ -1,70 +0,0 @@ -export const server_types = { - UNSUPPORTED: "unsupported", - MASTODON: "mastodon", - GLITCHSOC: "glitchsoc", - CHUCKYA: "chuckya", - FIREFISH: "firefish", - ICESHRIMP: "iceshrimp", - SHARKEY: "sharkey", -}; - -export const capabilities = { - MARKDOWN_CONTENT: "mdcontent", - REACTIONS: "reactions", -}; - -export class Instance { - host; - version; - capabilities; - type = server_types.UNSUPPORTED; - - constructor(host, version) { - this.host = host; - this.version = version; - this.#setType(version); - this.capabilities = this.#getCapabilities(this.type); - } - - #setType(version) { - this.type = server_types.UNSUPPORTED; - if (version.constructor !== String) return; - let version_lower = version.toLowerCase(); - for (let i = 1; i < Object.keys(server_types).length; i++) { - const check_type = Object.values(server_types)[i]; - if (version_lower.includes(check_type)) { - this.type = check_type; - return; - } - } - } - - #getCapabilities(type) { - let c = []; - switch (type) { - case server_types.MASTODON: - break; - case server_types.GLITCHSOC: - c.push(capabilities.REACTIONS); - break; - case server_types.CHUCKYA: - c.push(capabilities.REACTIONS); - break; - case server_types.FIREFISH: - c.push(capabilities.REACTIONS); - break; - case server_types.ICESHRIMP: - // more trouble than it's worth atm - // the server already hands this to us ;p - //c.push(capabilities.MARKDOWN_CONTENT); - c.push(capabilities.REACTIONS); - break; - case server_types.SHARKEY: - c.push(capabilities.REACTIONS); - break; - default: - break; - } - return c; - } -} diff --git a/src/lib/client/server.js b/src/lib/client/server.js new file mode 100644 index 0000000..6b42ffe --- /dev/null +++ b/src/lib/client/server.js @@ -0,0 +1,143 @@ +import * as api from '$lib/api.js'; +import { writable } from 'svelte/store'; +import { app_name } from '$lib/config.js'; +import { browser } from "$app/environment"; + +const server_types = { + UNSUPPORTED: "unsupported", + MASTODON: "mastodon", + GLITCHSOC: "glitchsoc", + CHUCKYA: "chuckya", + FIREFISH: "firefish", + ICESHRIMP: "iceshrimp", + SHARKEY: "sharkey", + AKKOMA: "akkoma", // TODO: verify + PLEROMA: "pleroma", // TODO: verify +}; + +export const capabilities = { + MARKDOWN_CONTENT: "markdown_content", + REACTIONS: "reactions", + FOREIGN_REACTIONS: "foreign_reactions", +}; + +// if server is falsy, assume user has not begun the login process. +export let server = writable(loadServer()); + +// write to localStorage on each update +server.subscribe(server => { + saveServer(server); +}); + +/** + * Attempts to create an server object using a given hostname. + * @param {string} host - The domain of the target server. + */ +export async function createServer(host) { + if (!host) { + console.error("Attempted to create server without providing a hostname"); + return false; + } + if (host.startsWith("http://")) { + console.error("Cowardly refusing to connect to an insecure server"); + return false; + } + + let server = {}; + server.host = host; + + if (host.startsWith("https://")) host = host.substring(8); + const data = await api.getInstance(host); + if (!data) { + console.error(`Failed to connect to ${host}`); + return false; + } + + server.version = data.version; + server.type = getType(server.version); + 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`); + } else { + console.log(`Server detected as "${server.type}" (${server.version}) with capabilities: {${server.capabilities.join(', ')}}`); + } + + return server; +} + +/** + * Saves the provided server to localStorage. + * If `server` is falsy, data is removed from localStorage. + * @param {Object} server + */ +function saveServer(server) { + if (!browser) return; + if (!server) { + localStorage.removeItem(app_name + "_server"); + return; + } + localStorage.setItem(app_name + "_server", JSON.stringify(server)); +} + +/** + * Returns server data loaded from localStorage, if it exists. + * Otherwise, returns false. + */ +function loadServer() { + if (!browser) return; + let data = localStorage.getItem(app_name + "_server"); + if (!data) return false; + return JSON.parse(data); +} + +/** + * Returns the type of an server, inferred from its version string. + * @param {string} version + * @returns the inferred server_type + */ +function getType(version) { + if (version.constructor !== String) return; + let version_lower = version.toLowerCase(); + for (let i = 1; i < Object.keys(server_types).length; i++) { + const type = Object.values(server_types)[i]; + if (version_lower.includes(type)) { + return type; + } + } + return server_types.UNSUPPORTED; +} + +/** + * Returns a list of capabilities for a given server_type. + * @param {string} type + */ +function getCapabilities(type) { + let c = []; + switch (type) { + case server_types.MASTODON: + break; + case server_types.GLITCHSOC: + c.push(capabilities.REACTIONS); + break; + case server_types.CHUCKYA: + c.push(capabilities.REACTIONS); + c.push(capabilities.FOREIGN_REACTIONS); + break; + case server_types.FIREFISH: + c.push(capabilities.REACTIONS); + break; + case server_types.ICESHRIMP: + // more trouble than it's worth atm + // mastodon API already hands html to us + //c.push(capabilities.MARKDOWN_CONTENT); + c.push(capabilities.REACTIONS); + break; + case server_types.SHARKEY: + c.push(capabilities.REACTIONS); + break; + default: + break; + } + return c; +} diff --git a/src/lib/config.js b/src/lib/config.js new file mode 100644 index 0000000..ccb3cdc --- /dev/null +++ b/src/lib/config.js @@ -0,0 +1 @@ +export const app_name = "campfire"; diff --git a/src/lib/emoji.js b/src/lib/emoji.js index 89df2d1..29385c3 100644 --- a/src/lib/emoji.js +++ b/src/lib/emoji.js @@ -1,52 +1,27 @@ -import { client } from './client/client.js'; import { get } from 'svelte/store'; +export const EMOJI_REGEX = /:[\w\-.]{0,32}:/g; -export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g; -export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g; - -export default class Emoji { - name; - url; - - constructor(id, name, host, url) { - this.id = id; - this.name = name; - this.host = host; - this.url = url; - } - - get html() { - if (this.url) - return `${this.name}`; - else - return `${this.name}`; - } +export function parseEmoji(shortcode, url) { + let emoji = { shortcode, url }; + if (emoji.shortcode == '❤') emoji.shortcode = '❤️'; // stupid heart unicode + emoji.html = `${emoji.shortcode}`; + return emoji; } -export function parseText(text, host) { +export function renderEmoji(text, emoji_list) { if (!text) return text; - let index = text.search(EMOJI_NAME_REGEX); + let index = text.search(EMOJI_REGEX); if (index === -1) return text; - // find the emoji name + // find the closing comma let length = text.substring(index + 1).search(':'); if (length <= 0) return text; - let emoji_name = text.substring(index + 1, index + length + 1); - let emoji = get(client).getEmoji(emoji_name + '@' + host); - if (emoji) { - return text.substring(0, index) + emoji.html + - parseText(text.substring(index + length + 2), host); - } - return text.substring(0, index + length + 1) + - parseText(text.substring(index + length + 1), host); -} + // see if emoji is valid + let shortcode = text.substring(index + 1, index + length + 1); + let emoji = emoji_list[shortcode]; + let replace = emoji ? emoji.html : shortcode; -export function parseOne(emoji_id) { - if (emoji_id == '❤') return '❤️'; // stupid heart unicode - if (EMOJI_REGEX.exec(':' + emoji_id + ':')) return emoji_id; - let cached_emoji = get(client).getEmoji(emoji_id); - if (!cached_emoji) return emoji_id; - return cached_emoji.html; + return text.substring(0, index) + replace + renderEmoji(text.substring(index + length + 2), emoji_list); } diff --git a/src/lib/notifications.js b/src/lib/notifications.js index bbdc69f..7798d29 100644 --- a/src/lib/notifications.js +++ b/src/lib/notifications.js @@ -1,40 +1,82 @@ -import { client } from '$lib/client/client.js'; -import * as api from '$lib/client/api.js'; +import * as api from '$lib/api.js'; +import { server } from '$lib/client/server.js'; +import { app } from '$lib/client/app.js'; +import { app_name } from '$lib/config.js'; import { get, writable } from 'svelte/store'; +import { browser } from '$app/environment'; +import { parsePost } from '$lib/post.js'; +import { parseAccount } from '$lib/account.js'; -export let notifications = writable([]); -export let unread_notif_count = writable(0); -export let last_read_notif_id = writable(0); +const prefix = app_name + '_notif_'; +const notification_limit = 40; -let loading; -export async function getNotifications() { - if (loading) return; // no spamming!! - loading = true; +export const notifications = writable([]); +export const unread_notif_count = writable(load("unread_count")); +export const last_read_notif_id = writable(load("last_read")); - api.getNotifications().then(async data => { - if (!data || data.length <= 0) return; - notifications.set([]); - for (let i in data) { - let notif = data[i]; - notif.accounts = [ await api.parseUser(notif.account) ]; - if (get(notifications).length > 0) { - let prev = get(notifications)[get(notifications).length - 1]; - if (notif.type === prev.type) { - if (prev.status && notif.status && prev.status.id === notif.status.id) { - notifications.update(notifications => { - notifications[notifications.length - 1].accounts.push(notif.accounts[0]); - return notifications; - }); - continue; - } +unread_notif_count.subscribe(count => save("unread_count", count)); +last_read_notif_id.subscribe(id => save("last_read", id)); + +/** + * Saves the provided data to localStorage. + * If `data` is falsy, the record is removed from localStorage. + * @param {Object} name + * @param {any} data + */ +function save(name, data) { + if (!browser) return; + if (data) { + localStorage.setItem(prefix + name, data); + } else { + localStorage.removeItem(prefix + name); + } +} + +/** + * Returns named data loaded from localStorage, if it exists. + * Otherwise, returns false. + */ +function load(name) { + if (!browser) return; + let data = localStorage.getItem(prefix + name); + return data ? data : false; +} + +export async function getNotifications(min_id, max_id) { + const new_notifications = await api.getNotifications( + get(server).host, + get(app).token, + min_id, + max_id, + notification_limit, + ); + + if (!new_notifications) { + console.error(`Failed to retrieve notifications.`); + loading = false; + return; + } + + for (let i in new_notifications) { + let notif = new_notifications[i]; + notif.accounts = [ await parseAccount(notif.account) ]; + + const _notifications = get(notifications); + if (_notifications.length > 0) { + let prev = _notifications[_notifications.length - 1]; + + if (notif.type === prev.type) { + if (prev.status && notif.status && prev.status.id === notif.status.id) { + notifications.update(notifications => { + notifications[notifications.length - 1].accounts.push(notif.accounts[0]); + return notifications; + }); + continue; } } - notif.status = notif.status ? await api.parsePost(notif.status, 0, false) : null; - notifications.update(notifications => [...notifications, notif]); } - last_read_notif_id.set(data[0].id); - unread_notif_count.set(0); - get(client).save(); - loading = false; - }); + + notif.status = notif.status ? await parsePost(notif.status, 0, false) : null; + notifications.update(notifications => [...notifications, notif]); + } } diff --git a/src/lib/post.js b/src/lib/post.js index 9b6d10f..66ccf34 100644 --- a/src/lib/post.js +++ b/src/lib/post.js @@ -1,177 +1,84 @@ -import { parseText as parseEmoji } from './emoji.js'; +import * as api from '$lib/api.js'; +import { server } from '$lib/client/server.js'; +import { app } from '$lib/client/app.js'; +import { parseAccount } from '$lib/account.js'; +import { parseEmoji, renderEmoji } from '$lib/emoji.js'; +import { get, writable } from 'svelte/store'; -export default class Post { - id; - created_at; - user; - text; - warning; - boost_count; - reply_count; - favourite_count; - favourited; - boosted; - mentions; - reactions; - emojis; - files; - url; - reply; - reply_id; - replies; - boost; - visibility; +const cache = writable({}); - async rich_text() { - return parseEmoji(this.text, this.user.host); +/** + * Parses a post using API data, and returns a writable store object. + * @param {Object} data + * @param {number} ancestor_count + */ +export async function parsePost(data, ancestor_count) { + let post = {}; + if (!ancestor_count) ancestor_count = 0; + + post.html = data.content; + + post.reply = null; + if ((data.in_reply_to_id || data.reply) && ancestor_count !== 0) { + const reply_data = data.reply || await api.getPost(get(server).host, get(app).token, data.in_reply_to_id); + // if the post returns false, we probably don't have permission to read it. + // we'll respect the thread's privacy, and leave it alone :) + if (!reply_data) return false; + post.reply = await parsePost(reply_data, ancestor_count - 1, false); } - /* - async rich_text() { - let text = this.text; - if (!text) return text; - let client = Client.get(); + post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; - const markdown_tokens = [ - { tag: "pre", token: "```" }, - { tag: "code", token: "`" }, - { tag: "strong", token: "**" }, - { tag: "strong", token: "__" }, - { tag: "em", token: "*" }, - { tag: "em", token: "_" }, - ]; + post.id = data.id; + post.created_at = new Date(data.created_at); + post.account = await parseAccount(data.account); + post.warning = data.spoiler_text; + post.reply_count = data.replies_count; + post.boost_count = data.reblogs_count; + post.boosted = data.reblogged; + post.favourite_count = data.favourites_count; + post.favourited = data.favourited; + post.mentions = data.mentions; + post.media = data.media_attachments; + post.url = data.url; + post.visibility = data.visibility; - let response = ""; - let md_layer; - let index = 0; - while (index < text.length) { - let sample = text.substring(index); - let md_nostack = !(md_layer && md_layer.nostack); - - // handle newlines - if (md_nostack && sample.startsWith('\n')) { - response += "
"; - index++; - continue; - } - - // handle mentions - if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT) - && md_nostack - && sample.match(/^@[\w\-.]+@[\w\-.]+/g) - ) { - // find end of the mention - let length = 1; - while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; - length++; // skim the middle @ - while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; - - let mention = text.substring(index, index + length); - - // attempt to resolve mention to a user - let user = await client.getUserByMention(mention); - if (user) { - const out = `` + - `` + - '@' + user.username + '@' + user.host + ""; - if (md_layer) md_layer.text += out; - else response += out; - } else { - response += mention; - } - index += mention.length; - continue; - } - - // handle links - if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT) - && md_nostack - && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g) - ) { - // get length of link - let length = text.substring(index).search(/\s|$/g); - let url = text.substring(index, index + length); - let out = `${url}`; - if (md_layer) md_layer.text += out; - else response += out; - index += length; - continue; - } - - // handle emojis - if (md_nostack && sample.match(/^:[\w\-.]{0,32}:/g)) { - // find the emoji name - let length = text.substring(index + 1).search(':'); - if (length <= 0) return text; - let emoji_name = text.substring(index + 1, index + length + 1); - let emoji = client.getEmoji(emoji_name + '@' + this.user.host); - - index += length + 2; - - if (!emoji) { - let out = ':' + emoji_name + ':'; - if (md_layer) md_layer.text += out; - else response += out; - continue; - } - - let out = emoji.html; - if (md_layer) md_layer.text += out; - else response += out; - continue; - } - - // handle markdown - // TODO: handle misskey-flavoured markdown(?) - if (md_layer) { - // try to pop layer - if (sample.startsWith(md_layer.token)) { - index += md_layer.token.length; - let out = `<${md_layer.tag}>${md_layer.text}`; - if (md_layer.token === '```') - out = `
${md_layer.text}
`; - if (md_layer.parent) md_layer.parent.text += out; - else response += out; - md_layer = md_layer.parent; - } else { - md_layer.text += sample[0]; - index++; - } - } else if (md_nostack) { - // should we add a layer? - let pushed = false; - for (let i = 0; i < markdown_tokens.length; i++) { - let item = markdown_tokens[i]; - if (sample.startsWith(item.token)) { - let new_md_layer = { - token: item.token, - tag: item.tag, - text: "", - parent: md_layer, - }; - if (item.token === '```' || item.token === '`') new_md_layer.nostack = true; - md_layer = new_md_layer; - pushed = true; - index += md_layer.token.length; - break; - } - } - if (!pushed) { - response += sample[0]; - index++; - } - } - } - - // destroy the remaining stack - while (md_layer) { - let out = md_layer.token + md_layer.text; - if (md_layer.parent) md_layer.parent.text += out; - else response += out; - md_layer = md_layer.parent; - } - - return response; + post.emojis = []; + if (post.emojis) { + data.emojis.forEach(emoji => { + post.emojis[emoji.shortcode] = parseEmoji(emoji.shortcode, emoji.url); + }); } - */ + + if (data.reactions) post.reactions = parseReactions(data.reactions); + + post.rich_text = renderEmoji(post.html, post.emojis); + + return post; + + // let cache_post = get(cache)[post.id]; + // if (cache_post) { + // cache_post.set(post); + // } else { + // cache.update(cache => { + // cache[post.id] = writable(post); + // return cache; + // }); + // } + + // return get(cache)[post.id]; +} + +export function parseReactions(data) { + let reactions = []; + data.forEach(reaction_data => { + let reaction = { + count: reaction_data.count, + name: reaction_data.name, + me: reaction_data.me, + }; + if (reaction_data.url) reaction.url = reaction_data.url; + reactions.push(reaction); + }); + return reactions; } diff --git a/src/lib/sound.js b/src/lib/sound.js index 3aa0e58..377b833 100644 --- a/src/lib/sound.js +++ b/src/lib/sound.js @@ -6,12 +6,12 @@ let sounds; if (typeof Audio !== typeof undefined) { sounds = { "default": new Audio(sound_log), - "post": new Audio(sound_hello), - "boost": new Audio(sound_success), + "post": new Audio(sound_success), + "boost": new Audio(sound_hello), }; } -export function play_sound(name) { +export function playSound(name) { if (name === false) return; if (!name) name = "default"; const sound = sounds[name]; diff --git a/src/lib/stores/account.js b/src/lib/stores/account.js new file mode 100644 index 0000000..5c6fecc --- /dev/null +++ b/src/lib/stores/account.js @@ -0,0 +1,3 @@ +import { writable } from 'svelte/store'; + +export let account = writable(false); diff --git a/src/lib/stores/compose.js b/src/lib/stores/compose.js new file mode 100644 index 0000000..2fe3b96 --- /dev/null +++ b/src/lib/stores/compose.js @@ -0,0 +1,4 @@ +import { writable } from 'svelte/store'; + +export const show = writable(false); +export const reply_post = writable(null); diff --git a/src/lib/stores/user.js b/src/lib/stores/user.js deleted file mode 100644 index fb9c2c4..0000000 --- a/src/lib/stores/user.js +++ /dev/null @@ -1,22 +0,0 @@ -import { client } from '$lib/client/client.js'; -import * as api from '$lib/client/api.js'; -import { get, writable } from 'svelte/store'; - -export let user = writable(0); -export let logged_in = writable(false); - -export async function getUser() { - // already known - if (get(user)) return get(user); - - // cannot provide- not logged in - if (!get(client).app || !get(client).app.token) return false; - - // logged in- attempt to retrieve using token - const data = await api.verifyCredentials(); - if (!data) return false; - - user.set(await api.parseUser(data)); - console.log(`Logged in as @${get(user).username}@${get(user).host}`); - return get(user); -} diff --git a/src/lib/timeline.js b/src/lib/timeline.js index 0ef7b8f..ae8a5e3 100644 --- a/src/lib/timeline.js +++ b/src/lib/timeline.js @@ -1,8 +1,10 @@ -import { client } from '$lib/client/client.js'; +import * as api from '$lib/api.js'; +import { server } from '$lib/client/server.js'; +import { app } from '$lib/client/app.js'; import { get, writable } from 'svelte/store'; -import { parsePost } from '$lib/client/api.js'; +import { parsePost } from '$lib/post.js'; -export let timeline = writable([]); +export const timeline = writable([]); let loading = false; @@ -10,9 +12,16 @@ export async function getTimeline(clean) { if (loading) return; // no spamming!! loading = true; - let timeline_data; - if (clean || get(timeline).length === 0) timeline_data = await get(client).getTimeline() - else timeline_data = await get(client).getTimeline(get(timeline)[get(timeline).length - 1].id); + let last_post = false; + if (!clean && get(timeline).length > 0) + last_post = get(timeline)[get(timeline).length - 1].id; + + const timeline_data = await api.getTimeline( + get(server).host, + get(app).token, + "home", + last_post + ); if (!timeline_data) { console.error(`Failed to retrieve timeline.`); @@ -24,7 +33,7 @@ export async function getTimeline(clean) { for (let i in timeline_data) { const post_data = timeline_data[i]; - const post = await parsePost(post_data, 1, false); + const post = await parsePost(post_data, 1); if (!post) { if (post === null || post === undefined) { if (post_data.id) { diff --git a/src/lib/ui/Button.svelte b/src/lib/ui/Button.svelte index ea68fc4..22e4d87 100644 --- a/src/lib/ui/Button.svelte +++ b/src/lib/ui/Button.svelte @@ -1,5 +1,5 @@ + +
+
+ + + +
+ +
+
+
+
+ +
+
+ {#if show_cw} + + {/if} + + +
+ + diff --git a/src/lib/ui/Feed.svelte b/src/lib/ui/Feed.svelte deleted file mode 100644 index a5c5d84..0000000 --- a/src/lib/ui/Feed.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - -
-

Home

- -
- -
- {#if posts.length <= 0} -
- getting the feed... -
- {/if} - {#each posts as post} - - {/each} -
- - diff --git a/src/lib/ui/LoginForm.svelte b/src/lib/ui/LoginForm.svelte index 8cf9b07..1a64f88 100644 --- a/src/lib/ui/LoginForm.svelte +++ b/src/lib/ui/LoginForm.svelte @@ -1,37 +1,42 @@ @@ -40,11 +45,11 @@

Welcome, fediverse user!

-

Please enter your instance domain to log in.

+

Please enter your server domain to log in.

- - {#if instance_url_error} -

{instance_url_error}

+ + {#if display_error} +

{display_error}

{/if}

@@ -70,6 +75,10 @@ text-align: center; } + .app-logo :global(svg) { + width: 100%; + } + .input-wrapper { width: 360px; margin: 0 auto; diff --git a/src/lib/ui/Modal.svelte b/src/lib/ui/Modal.svelte new file mode 100644 index 0000000..3ac3727 --- /dev/null +++ b/src/lib/ui/Modal.svelte @@ -0,0 +1,97 @@ + + +{#if visible} +
visible = !visible}>
+
+ +
+{/if} + + diff --git a/src/lib/ui/Navigation.svelte b/src/lib/ui/Navigation.svelte index c4ce6e5..a5bedfd 100644 --- a/src/lib/ui/Navigation.svelte +++ b/src/lib/ui/Navigation.svelte @@ -1,17 +1,18 @@
@@ -151,16 +169,17 @@
- play_sound()}> + playSound()}>
{/if} + campfire v{VERSION}
@@ -183,7 +202,7 @@ background-color: var(--bg-800); } - .instance-header { + .server-header { width: 100%; height: 172px; display: flex; @@ -196,7 +215,7 @@ background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); } - .instance-icon { + .server-icon { height: 50%; border-radius: 8px; } @@ -345,6 +364,11 @@ font-size: .65em; } + .nickname :global(.emoji) { + height: 1.2em; + margin: -.1em 0; + } + .flex-row { display: flex; flex-direction: row; diff --git a/src/lib/ui/Notification.svelte b/src/lib/ui/Notification.svelte index f16abec..b44b63e 100644 --- a/src/lib/ui/Notification.svelte +++ b/src/lib/ui/Notification.svelte @@ -1,12 +1,12 @@
{#if data.status}
- {@html data.status.html} + {#if data.status.warning} +
+ {data.status.warning} +
+ {:else} + {@html data.status.rich_text} + {/if} + + {#if data.status.media && data.status.media.length > 0} +
+ {#each data.status.media as media} +
+ {#if ["image", "gifv", "gif"].includes(media.type)} + + {media.description} + + {:else if media.type === "video"} + + {/if} +
+ {/each} +
+ {/if}
{#if data.type === "mention"} {#if data.status.reactions} @@ -122,6 +148,7 @@ text-decoration: inherit; color: inherit; transition: background-color .1s; + cursor: pointer; } .notification:hover { @@ -154,6 +181,7 @@ } header .notif-avatars img:not(:first-child) { box-shadow: 4px 0 8px -2px rgba(0,0,0,.33); + z-index: 0; } header .notif-avatars img:not(:last-child) { margin-left: -8px; @@ -247,4 +275,52 @@ margin-right: 4px; border-radius: 4px; } + + .notif-content .warning { + width: calc(100% - 16px); + margin-bottom: 10px; + padding: 4px 8px; + --warn-bg: color-mix(in srgb, var(--bg-700), var(--accent) 1%); + background: repeating-linear-gradient(-45deg, transparent, transparent 10px, var(--warn-bg) 10px, var(--warn-bg) 20px); + font-family: inherit; + font-size: inherit; + color: inherit; + text-align: left; + border: none; + border-radius: 8px; + cursor: pointer; + outline-color: var(--warn-bg); + transition: outline .05s, box-shadow .05s; + } + + .notif-media-container { + margin: 16px 0 4px 0; + display: flex; + flex-direction: row; + gap: 8px; + font-size: 14px; + line-height: 1.45em; + } + + .notif-media { + display: inline-block; + border-radius: 12px; + background-color: #000; + overflow: hidden; + } + + .notif-media a { + width: 5em; + height: 5em; + display: block; + cursor: zoom-in; + } + + .notif-media img, + .notif-media video { + width: 100%; + height: 100%; + display: block; + object-fit: cover; + } diff --git a/src/lib/ui/post/ActionBar.svelte b/src/lib/ui/post/ActionBar.svelte index 2b4c579..8d86ba8 100644 --- a/src/lib/ui/post/ActionBar.svelte +++ b/src/lib/ui/post/ActionBar.svelte @@ -1,7 +1,11 @@ -
+ diff --git a/src/lib/ui/post/ReactionButton.svelte b/src/lib/ui/post/ReactionButton.svelte index fee3a97..6b933b1 100644 --- a/src/lib/ui/post/ReactionButton.svelte +++ b/src/lib/ui/post/ReactionButton.svelte @@ -1,5 +1,5 @@ @@ -49,6 +49,7 @@ border-radius: 8px; transition: background-color .1s, color .1s; cursor: pointer; + border: 1px solid var(--bg-700); } button.active { @@ -72,7 +73,7 @@ } .icon { - width: 20px; + min-width: 20px; height: 20px; display: flex; justify-content: center; diff --git a/src/lib/ui/post/ReplyContext.svelte b/src/lib/ui/post/ReplyContext.svelte index 20d7ab4..0474460 100644 --- a/src/lib/ui/post/ReplyContext.svelte +++ b/src/lib/ui/post/ReplyContext.svelte @@ -1,18 +1,15 @@ diff --git a/src/lib/user/user.js b/src/lib/user/user.js deleted file mode 100644 index 050572b..0000000 --- a/src/lib/user/user.js +++ /dev/null @@ -1,29 +0,0 @@ -import { client } from '../client/client.js'; -import { parseText as parseEmojis } from '../emoji.js'; -import { get } from 'svelte/store'; - -export default class User { - id; - nickname; - username; - host; - avatar_url; - emojis; - url; - - get name() { - return this.nickname || this.username; - } - - get mention() { - let res = "@" + this.username; - if (this.host != get(client).instance.host) - res += "@" + this.host; - return res; - } - - get rich_name() { - if (!this.nickname) return this.username; - return parseEmojis(this.nickname, this.host); - } -} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 27eab4b..aec6b96 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,49 +1,52 @@
-
- + show_composer = true} />
- {#await ready} + {#await init()}
just a moment...
@@ -56,6 +59,9 @@
+ + show_composer = false }/> +
diff --git a/src/routes/post/[id]/+page.js b/src/routes/[server]/+page.js similarity index 63% rename from src/routes/post/[id]/+page.js rename to src/routes/[server]/+page.js index 9291873..bc936af 100644 --- a/src/routes/post/[id]/+page.js +++ b/src/routes/[server]/+page.js @@ -1,5 +1,5 @@ export async function load({ params }) { return { - post_id: params.id + server_domain: params.server }; } diff --git a/src/routes/[server]/[account]/+page.js b/src/routes/[server]/[account]/+page.js new file mode 100644 index 0000000..edbfd18 --- /dev/null +++ b/src/routes/[server]/[account]/+page.js @@ -0,0 +1,8 @@ +import { error } from '@sveltejs/kit'; + +export async function load({ params }) { + return error(404, 'Not Found'); + // return { + // account_name: params.account + // }; +} diff --git a/src/routes/[server]/[account]/[post]/+page.js b/src/routes/[server]/[account]/[post]/+page.js new file mode 100644 index 0000000..0cd52c3 --- /dev/null +++ b/src/routes/[server]/[account]/[post]/+page.js @@ -0,0 +1,7 @@ +export async function load({ params }) { + return { + server_host: params.server, + account_handle: params.account, + post_id: params.post + }; +} diff --git a/src/routes/[server]/[account]/[post]/+page.svelte b/src/routes/[server]/[account]/[post]/+page.svelte new file mode 100644 index 0000000..75daa65 --- /dev/null +++ b/src/routes/[server]/[account]/[post]/+page.svelte @@ -0,0 +1,146 @@ + + +{#await post} +
+ loading post... +
+{:then post} + {#if error} +

{@html error}

+ {:else} +
+ {#if previous_page} + + {/if} + +

+ Post by {@html post.account.rich_name} +

+
+ +
+ +
+ {#each post.replies as reply} + {#await reply then reply} + + {/await} + {/each} +
+ {/if} +{/await} + + diff --git a/src/routes/callback/+page.svelte b/src/routes/callback/+page.svelte index 9540cb8..8b6985e 100644 --- a/src/routes/callback/+page.svelte +++ b/src/routes/callback/+page.svelte @@ -1,42 +1,46 @@
diff --git a/src/routes/post/+page.js b/src/routes/post/+page.js deleted file mode 100644 index c0ac9bd..0000000 --- a/src/routes/post/+page.js +++ /dev/null @@ -1,5 +0,0 @@ -import { error } from '@sveltejs/kit'; - -export function load(event) { - error(404, 'Not Found'); -} diff --git a/src/routes/post/[id]/+page.svelte b/src/routes/post/[id]/+page.svelte deleted file mode 100644 index e2e27aa..0000000 --- a/src/routes/post/[id]/+page.svelte +++ /dev/null @@ -1,132 +0,0 @@ - - -{#if !error} -
- {#await post then post} - - -

- Post by {@html post.user.rich_name} -

- {/await} -
- -
- {#await post} -
- loading post... -
- {:then post} - -
- {#each post.replies as reply} - {#await reply then reply} - - {/await} - {/each} - {/await} -
-{:else} -

{@html error}

-{/if} - - diff --git a/svelte.config.js b/svelte.config.js index b371a21..9c24c44 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -17,6 +17,11 @@ const config = { }), version: { 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" } }, };