diff --git a/CHANGELOGS.md b/CHANGELOGS.md new file mode 100644 index 0000000..d5cee41 --- /dev/null +++ b/CHANGELOGS.md @@ -0,0 +1,23 @@ +# 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 + +# Campfire v0.2.0 +- Complete UI overhaul (thanks mae!) +- Added light and dark themes +- Added ability to like and boost posts +- Added ability to view threads in context +- Many background tweaks, fixes, and optimisations diff --git a/README.md b/README.md index 9ea7d71..2272f69 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,13 @@ will likely be incompatible, and the web console will get very upset. - log in with most/all fedi services! (with varying compatibility) - ability to favourite, boost, and react to posts - view threads in context +- notifications feed - fun, clicky buttons! ### planned - posting! (incl. replies and quotes) - live feed -- notifications feed ### "maybe" diff --git a/package-lock.json b/package-lock.json index 2388e03..ef4c2d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "campfire-client", - "version": "0.2.0_rev3", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "campfire-client", - "version": "0.2.0_rev3", + "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 80a43c1..e039bc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "campfire-client", - "version": "0.2.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/app.html b/src/app.html index ce47f4e..9f07942 100644 --- a/src/app.html +++ b/src/app.html @@ -12,14 +12,14 @@ - + - + %sveltekit.head% 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 8e47670..5a74811 100644 --- a/src/img/icons/quote.svg +++ b/src/img/icons/quote.svg @@ -1,3 +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 b2fcd4c..d6656b4 100644 --- a/src/lib/app.css +++ b/src/lib/app.css @@ -36,7 +36,6 @@ } body { - width: 100vw; margin: 0; padding: 0; @@ -75,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 27f0e0b..0000000 --- a/src/lib/client/api.js +++ /dev/null @@ -1,364 +0,0 @@ -import { Client } from '../client/client.js'; -import { capabilities } from '../client/instance.js'; -import Post from '../post.js'; -import User from '../user/user.js'; -import Emoji from '../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() { - let client = get(Client.get()); - return `https://${client.instance.host}/oauth/authorize` + - `?client_id=${client.app.id}` + - "&scope=read+write+push" + - `&redirect_uri=${location.origin}/callback` + - "&response_type=code"; -} - -export async function getToken(code) { - let client = get(Client.get()); - let form = new FormData(); - form.append("client_id", client.app.id); - form.append("client_secret", 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://${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 client = get(Client.get()); - let form = new FormData(); - form.append("client_id", client.app.id); - form.append("client_secret", client.app.secret); - form.append("token", client.app.token); - - const res = await fetch(`https://${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 client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/accounts/verify_credentials`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + client.app.token } - }).then(res => res.json()); - - return data; -} - -export async function getTimeline(last_post_id) { - let client = get(Client.get()); - let url = `https://${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 " + client.app.token } - }).then(res => res.json()); - - return data; -} - -export async function getPost(post_id, ancestor_count) { - let client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + 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 client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/context`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + 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 client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/reblog`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + 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 client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unreblog`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + 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 client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/favourite`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + 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 client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unfavourite`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + 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 client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + 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 client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`; - const data = await fetch(url, { - method: 'POST', - headers: { "Authorization": "Bearer " + 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, with_context) { - let client = get(Client.get()); - let post = new Post(); - - post.text = data.content; - - post.reply = null; - if (!with_context && // ancestor replies are handled in full later - (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.replies = []; - if (with_context) { - const replies_data = await getPostContext(data.id); - if (replies_data) { - // posts this is replying to - if (replies_data.ancestors) { - let head = post; - while (replies_data.ancestors.length > 0) { - head.reply = await parsePost(replies_data.ancestors.pop(), 0, false); - head = head.reply; - } - } - // posts in reply to this - if (replies_data.descendants) { - for (let i in replies_data.descendants) { - post.replies.push(await parsePost(replies_data.descendants[i], 0, false)); - } - } - } - } - - 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 && 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 client = get(Client.get()); - let user = await client.getCacheUser(data.id); - - if (user) return user; - // cache miss! - - user = new User(); - user.id = data.id; - user.nickname = data.display_name; - 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 = 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)); - }); - - client.putCacheUser(user); - return user; -} - -export function parseReactions(data) { - let client = get(Client.get()); - 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.get()).putCacheEmoji(emoji); - return emoji; -} - -export async function getUser(user_id) { - let client = get(Client.get()); - let url = `https://${client.instance.host}/api/v1/accounts/${user_id}`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + client.app.token } - }).then(res => res.json()); - - const user = await parseUser(data); - if (user === null || user === undefined) { - if (data.id) { - console.warn("Failed to parse user data #" + data.id); - } else { - console.warn("Failed to parse user data:"); - console.warn(data); - } - return false; - } - return user; -} 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 f4e3788..0000000 --- a/src/lib/client/client.js +++ /dev/null @@ -1,205 +0,0 @@ -import { Instance, server_types } from './instance.js'; -import * as api from './api.js'; -import { get, writable } from 'svelte/store'; - -let client = writable(false); - -const save_name = "campfire"; - -export class Client { - instance; - app; - user; - #cache; - - constructor() { - this.instance = null; - this.app = null; - this.user = null; - this.cache = { - users: {}, - emojis: {}, - }; - } - - static get() { - let current = get(client); - if (current && current.app) return client; - let new_client = new Client(); - new_client.load(); - client.set(new_client); - return client; - } - - 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; - } - this.app.token = token; - client.set(this); - } - - async revokeToken() { - return await api.revokeToken(); - } - - async verifyCredentials() { - if (this.user) return this.user; - if (!this.app || !this.app.token) { - this.user = false; - return false; - } - const data = await api.verifyCredentials(); - if (!data) { - this.user = false; - return false; - } - await client.update(async c => { - c.user = await api.parseUser(data); - console.log(`Logged in as @${c.user.username}@${c.user.host}`); - }); - return this.user; - } - - 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 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; - } - - 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, - }, - 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); - 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); - client.set(false); - 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 4fdd161..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.get()).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.get()).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 new file mode 100644 index 0000000..7798d29 --- /dev/null +++ b/src/lib/notifications.js @@ -0,0 +1,82 @@ +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'; + +const prefix = app_name + '_notif_'; +const notification_limit = 40; + +export const notifications = writable([]); +export const unread_notif_count = writable(load("unread_count")); +export const last_read_notif_id = writable(load("last_read")); + +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 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/timeline.js b/src/lib/timeline.js index 5858199..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 posts = writable([]); +export const timeline = writable([]); let loading = false; @@ -10,11 +12,16 @@ export async function getTimeline(clean) { if (loading) return; // no spamming!! loading = true; - let client = get(Client.get()); + let last_post = false; + if (!clean && get(timeline).length > 0) + last_post = get(timeline)[get(timeline).length - 1].id; - let timeline_data; - if (clean || get(posts).length === 0) timeline_data = await client.getTimeline() - else timeline_data = await client.getTimeline(get(posts)[get(posts).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.`); @@ -22,11 +29,11 @@ export async function getTimeline(clean) { return; } - if (clean) posts.set([]); + if (clean) timeline.set([]); 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) { @@ -38,7 +45,7 @@ export async function getTimeline(clean) { } continue; } - posts.update(current => [...current, post]); + timeline.update(current => [...current, post]); } loading = false; } diff --git a/src/lib/ui/Button.svelte b/src/lib/ui/Button.svelte index 702d3fa..22e4d87 100644 --- a/src/lib/ui/Button.svelte +++ b/src/lib/ui/Button.svelte @@ -1,6 +1,8 @@ + + + {#if show_cw} + + {/if} + + + + + diff --git a/src/lib/ui/Feed.svelte b/src/lib/ui/Feed.svelte deleted file mode 100644 index bd3cef4..0000000 --- a/src/lib/ui/Feed.svelte +++ /dev/null @@ -1,61 +0,0 @@ - - -
- {#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 new file mode 100644 index 0000000..1a64f88 --- /dev/null +++ b/src/lib/ui/LoginForm.svelte @@ -0,0 +1,171 @@ + + +
+ +

Welcome, fediverse user!

+

Please enter your server domain to log in.

+
+ + {#if display_error} +

{display_error}

+ {/if} +
+
+ +

+ Please note this is + extremely experimental software; + things are likely to break! +
+ If that's all cool with you, welcome aboard! +

+ + +
+ + 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 a513649..a5bedfd 100644 --- a/src/lib/ui/Navigation.svelte +++ b/src/lib/ui/Navigation.svelte @@ -1,12 +1,18 @@ - {#if (client.user)}
- play_sound()}> + playSound()}>
{/if} + campfire v{VERSION}
@@ -170,7 +202,7 @@ background-color: var(--bg-800); } - .instance-header { + .server-header { width: 100%; height: 172px; display: flex; @@ -183,7 +215,7 @@ background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); } - .instance-icon { + .server-icon { height: 50%; border-radius: 8px; } @@ -212,6 +244,7 @@ transform: translate(22px, -16px); min-width: 12px; height: 28px; + margin-left: auto; padding: 0 8px; display: flex; justify-content: center; @@ -323,6 +356,7 @@ overflow: hidden; white-space: nowrap; font-size: .8em; + color: inherit; } .username { @@ -330,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 new file mode 100644 index 0000000..b44b63e --- /dev/null +++ b/src/lib/ui/Notification.svelte @@ -0,0 +1,326 @@ + + +
{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)}} + on:keydown={gotoPost}> +
+ + {#if data.type === "favourite"} + + {:else if data.type === "reblog"} + + {:else if data.type === "reaction"} + + {:else if data.type === "mention"} + + {:else} + + {/if} + + + {#if data.accounts.length == 1} + + + + {:else} + {#each accounts_short as account} + + {/each} + {/if} + + {@html activity_text.replace("%1", mention(data.accounts))} + +
+ {#if data.status} +
+ {#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} + + {/if} + + {/if} + {/if} +
+ + diff --git a/src/lib/ui/post/ActionBar.svelte b/src/lib/ui/post/ActionBar.svelte index 83a4781..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 ad4f620..0474460 100644 --- a/src/lib/ui/post/ReplyContext.svelte +++ b/src/lib/ui/post/ReplyContext.svelte @@ -1,39 +1,42 @@ {#if post.reply} - + {#await post.reply then reply} + + {/await} {/if}
{mouse_pos.left = e.pageX; mouse_pos.top = e.pageY; console.log(mouse_pos)}} - on:mouseup={e => {if (e.pageX == mouse_pos.left && e.pageY == mouse_pos.top) gotoPost()}} + 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)}} on:keydown={gotoPost}>
@@ -43,7 +46,9 @@
- + {#if post.reactions} + + {/if}
diff --git a/src/lib/user/user.js b/src/lib/user/user.js deleted file mode 100644 index 7b91fe8..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.get()).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 9f7c10f..aec6b96 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,21 +1,52 @@
-
- + show_composer = true} />
- {#await client.verifyCredentials()} + {#await init()}
just a moment...
@@ -28,4 +59,19 @@
+ + show_composer = false }/> + + + diff --git a/src/routes/+page.js b/src/routes/+page.js index 8a7c3ff..ceccaaf 100644 --- a/src/routes/+page.js +++ b/src/routes/+page.js @@ -1,15 +1,2 @@ -import Feed from '$lib/ui/Feed.svelte'; -import { Client } from '$lib/client/client.js'; -import Button from '$lib/ui/Button.svelte'; -import { get } from 'svelte/store'; - export const prerender = true; export const ssr = false; - -export async function load() { - let client = get(Client.get()); - await client.verifyCredentials(); - return { - client: client - }; -} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5c94613..156a07b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,45 +1,25 @@ -{#if logged_in} +{#if $account}

Home

- +
+ {#if $timeline.length <= 0} +
+ getting the feed... +
+ {/if} + {#each $timeline as post} + + {/each} +
{:else} -
- -

Welcome, fediverse user!

-

Please enter your instance domain to log in.

-
- - {#if instance_url_error} -

{instance_url_error}

- {/if} -
-
- -

- Please note this is - extremely experimental software; - things are likely to break! -
- If that's all cool with you, welcome aboard! -

- - -
+ {/if} diff --git a/src/routes/[server]/+page.js b/src/routes/[server]/+page.js new file mode 100644 index 0000000..bc936af --- /dev/null +++ b/src/routes/[server]/+page.js @@ -0,0 +1,5 @@ +export async function load({ params }) { + return { + 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.js b/src/routes/callback/+page.js index 80122d2..addf7b8 100644 --- a/src/routes/callback/+page.js +++ b/src/routes/callback/+page.js @@ -1,20 +1,5 @@ -import { Client } from '$lib/client/client.js'; -import { goto } from '$app/navigation'; -import { error } from '@sveltejs/kit'; -import { get } from 'svelte/store'; - -export const ssr = false; - -export async function load({ params, url }) { - const client = get(Client.get()); - let auth_code = url.searchParams.get("code"); - if (auth_code) { - client.getToken(auth_code).then(() => { - client.save(); - goto("/"); - }); - } - error(400, { - message: "Bad request" - }); +export async function load({ url }) { + return { + code: url.searchParams.get("code") || false + }; } diff --git a/src/routes/callback/+page.svelte b/src/routes/callback/+page.svelte new file mode 100644 index 0000000..8b6985e --- /dev/null +++ b/src/routes/callback/+page.svelte @@ -0,0 +1,49 @@ + diff --git a/src/routes/notifications/+page.svelte b/src/routes/notifications/+page.svelte new file mode 100644 index 0000000..c6294d8 --- /dev/null +++ b/src/routes/notifications/+page.svelte @@ -0,0 +1,71 @@ + + +
+

Notifications

+
+ +
+ {#if $notifications.length === 0} +
+ fetching notifications... +
+ {:else} + {#each $notifications as notif} + + {/each} + {/if} +
+ + 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.js b/src/routes/post/[id]/+page.js deleted file mode 100644 index 30569fd..0000000 --- a/src/routes/post/[id]/+page.js +++ /dev/null @@ -1,39 +0,0 @@ -import Post from '$lib/ui/post/Post.svelte'; -import { Client } from '$lib/client/client.js'; -import { parsePost } from '$lib/client/api.js'; -import { get } from 'svelte/store'; -import { goto } from '$app/navigation'; - -export const ssr = false; - -export async function load({ params }) { - let client = get(Client.get()); - - if (!client.instance || !client.user) { - goto("/"); - } - - const post_id = params.id; - - const post_data = await client.getPost(post_id); - if (!post_data) { - console.error(`Failed to retrieve post ${post_id}.`); - return null; - } - - const post = await parsePost(post_data, 10, true); - let posts = [post]; - for (let i in post.replies) { - const reply = post.replies[i]; - // if (i > 1 && reply.reply_id === post.replies[i - 1].id) { - // let reply_head = posts.pop(); - // reply.reply = reply_head; - // } - posts.push(reply); - // console.log(reply); - } - - return { - posts: posts - }; -} diff --git a/src/routes/post/[id]/+page.svelte b/src/routes/post/[id]/+page.svelte deleted file mode 100644 index a5e9edc..0000000 --- a/src/routes/post/[id]/+page.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - -
-

Home

- -
- -
- {#if data.posts.length <= 0} -
- just a moment... -
- {:else} - {#key data} - -
- {#each replies as post} - - {/each} - {/key} - {/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" } }, };