Compare commits

...

75 Commits

Author SHA1 Message Date
naskya dd67aff9f1 Merge branch 'refactor/push-notification' into 'develop'
Draft: refactor: port push notification sender to backend-rs


See merge request firefish/firefish!10760
2024-05-07 21:12:13 +00:00
naskya c3374b4914
Merge branch 'develop' into refactor/push-notification 2024-05-08 06:11:52 +09:00
naskya cda31d3dc7
Revert "refactor (backend): port publishNotesStream to backend-rs"
This reverts commit 5382dc5da8.

It turns out this sends an inccorect time info to the stream
since JavaScript's Date object doesn't have timezone info

I'll revisit this in the future
2024-05-08 06:08:26 +09:00
naskya 907578e8f8
ci: fix config error 2024-05-08 05:28:41 +09:00
naskya 2923ea86de
ci: update workflow rules 2024-05-08 05:26:59 +09:00
naskya 226c990385
ci: use buildah caches 2024-05-08 05:26:36 +09:00
naskya 769f52c8ee Merge branch 'fix/reactive' into 'develop'
fix: use reactive MkTime

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10796
2024-05-07 19:59:12 +00:00
naskya 8a00d82f36
ci: add firefish-js 2024-05-08 04:49:13 +09:00
naskya 34ed877f57
ci: don't build the backend on client-only changes 2024-05-08 04:41:20 +09:00
Lhcfl f5074f35cc fix: use reactive MkTime 2024-05-08 03:00:07 +08:00
naskya a847dd55ad
ci: fix cargo clippy task 2024-05-08 03:58:21 +09:00
naskya 7a31465b8a
Merge branch 'develop' into refactor/push-notification 2024-05-08 02:44:22 +09:00
naskya 5382dc5da8
refactor (backend): port publishNotesStream to backend-rs 2024-05-08 02:15:07 +09:00
naskya 989e93f2a0
fix: migrate back from happy-dom to JSDOM (closes #10924 #10914 #10842)
this reverts commit 4565867b8b.
2024-05-08 01:52:15 +09:00
naskya df81cb6a85 Merge branch 'feat/collepse-reply-timeline' into 'develop'
feat: collepse renotes and replies in timeline

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

Closes #10908

See merge request firefish/firefish!10788
2024-05-07 16:20:45 +00:00
Lhcfl 31168cc7b2 fix: use reacive MkSubNoteContent 2024-05-07 23:42:40 +08:00
Lhcfl 42886f054d fix: use reactive previewableCount 2024-05-07 23:31:45 +08:00
Lhcfl 1d0ea11eea fix: use note capture in MkNoteSimple 2024-05-07 23:23:19 +08:00
Lhcfl 24602c4745 update locales 2024-05-07 22:49:09 +08:00
Lhcfl 33923a59fa fix: use reactive MkNoteHeader 2024-05-07 22:37:09 +08:00
Lhcfl 8067ed4084 Merge branch 'develop' of https://firefish.dev/firefish/firefish into feat/collepse-reply-timeline 2024-05-07 22:34:45 +08:00
naskya 4277ad0b59
meta: update COPYING & include LICENSE in pre-built images 2024-05-07 20:54:47 +09:00
naskya fc65d8c1c3
docs: update api-change.md 2024-05-07 20:52:11 +09:00
naskya c6daa2fcf9
Merge branch 'develop' into refactor/push-notification 2024-05-07 02:16:13 +09:00
naskya e68fba0649
Merge branch 'develop' into refactor/push-notification 2024-05-06 08:30:28 +09:00
naskya af70150604
Merge branch 'develop' into refactor/push-notification 2024-05-06 03:15:59 +09:00
naskya fc1078f52c
Merge branch 'develop' into refactor/push-notification 2024-05-05 14:58:34 +09:00
naskya 66d59bd8ef
Merge branch 'develop' into refactor/push-notification 2024-05-04 13:22:34 +09:00
Lhcfl 46d0679845 little patch 2024-05-03 00:56:10 +08:00
Lhcfl 160e7f26a6 feat: collepse renotes and replies 2024-05-03 00:22:25 +08:00
naskya 8dd4e5ed64
Merge branch 'develop' into refactor/push-notification 2024-05-02 22:43:30 +09:00
naskya cce45303a1
Merge branch 'develop' into refactor/push-notification 2024-05-02 19:33:59 +09:00
Lhcfl 9138c3726a dev: use reactiveState in foldNotification 2024-05-02 01:07:57 +08:00
Lhcfl 425b333474 set collapseReplyInTimeline default to false 2024-05-02 00:57:00 +08:00
Lhcfl d1c76b3882 feat: allow collepse replied posts in timeline 2024-05-02 00:53:52 +08:00
naskya 7449802409
Merge branch 'develop' into refactor/push-notification 2024-05-01 14:11:40 +09:00
naskya e26b75c5c8
refactor (backend-rs): remove duplicate code 2024-05-01 13:13:10 +09:00
naskya c13d6344e1
Merge branch 'develop' into refactor/push-notification 2024-04-30 06:53:29 +09:00
naskya a2ca239a00
chore: apply clippy fix 2024-04-29 11:52:49 +09:00
naskya 0189684fec
Merge branch 'develop' into refactor/push-notification 2024-04-29 10:53:42 +09:00
naskya 984896b380
Merge branch 'develop' into refactor/push-notification 2024-04-28 14:19:23 +09:00
naskya 34bf6f95d4
Merge branch 'develop' into refactor/push-notification 2024-04-27 21:32:52 +09:00
naskya 1f15d4382f
Merge branch 'develop' into refactor/push-notification 2024-04-27 18:54:22 +09:00
naskya 87c9c76117
Merge branch 'develop' into refactor/push-notification 2024-04-27 11:03:05 +09:00
naskya ff96c20893
Merge branch 'develop' into refactor/push-notification 2024-04-27 06:06:53 +09:00
naskya 56f06bae56
Merge branch 'develop' into refactor/push-notification 2024-04-26 13:59:24 +09:00
naskya a4e6964d2f
chore (backend-rs): static -> const 2024-04-26 06:34:09 +09:00
naskya d9028a786f
Merge branch 'develop' into refactor/push-notification 2024-04-26 04:06:29 +09:00
naskya c6d30f8026
Merge branch 'develop' into refactor/push-notification 2024-04-25 13:10:55 +09:00
naskya 2a34c005a3
Merge branch 'develop' into refactor/push-notification 2024-04-25 11:42:33 +09:00
naskya 62bc04fcc9
Merge branch 'develop' into refactor/push-notification 2024-04-25 11:09:00 +09:00
naskya 793007358a
Merge branch 'develop' into refactor/push-notification 2024-04-25 10:59:05 +09:00
naskya af20b6834f
chore (backend-rs): more error handlings for push notifications 2024-04-25 10:34:27 +09:00
naskya 831653ca54
chore: format 2024-04-25 10:34:04 +09:00
naskya 61fb36d041
Merge branch 'develop' into refactor/push-notification 2024-04-25 10:01:51 +09:00
naskya 320b1ecb83
Merge branch 'develop' into refactor/push-notification 2024-04-25 08:22:45 +09:00
naskya da12915448
Merge branch 'develop' into refactor/push-notification 2024-04-25 08:06:59 +09:00
naskya e71b587888
Merge branch 'develop' into refactor/push-notification 2024-04-25 02:29:22 +09:00
naskya 1e310101a3
Merge branch 'develop' into refactor/push-notification 2024-04-24 15:45:57 +09:00
naskya 854030db3b
Merge branch 'develop' into refactor/push-notification 2024-04-24 13:30:50 +09:00
naskya d8a6631f16
Merge branch 'develop' into refactor/push-notification 2024-04-24 04:22:22 +09:00
naskya bda7924672
Merge branch 'develop' into refactor/push-notification 2024-04-24 00:25:53 +09:00
naskya a14078283c
perf (backend-rs): reuse web push client 2024-04-24 00:00:11 +09:00
naskya 4b94af9944
fix (backend): fix push notification payload 2024-04-24 00:00:11 +09:00
naskya 3e96465569
feat (backend-rs): delete subscriptions on failed web push 2024-04-24 00:00:11 +09:00
naskya 01ce68ef60
fix (backend): fix push notification payload 2024-04-24 00:00:11 +09:00
naskya 5c6e0ef027
chore (backend): move stuff outside of a loop 2024-04-24 00:00:11 +09:00
naskya 4a6377f019
fix (backend): fix push notification payload 2024-04-24 00:00:11 +09:00
naskya fc2864b3a2
fix (backend-rs): set longer ttl for push notifications 2024-04-24 00:00:10 +09:00
naskya 3f399bc067
fix (backend-rs): convert base64 encoded keys into base64url 2024-04-24 00:00:10 +09:00
naskya 6442364341
fix (backend-rs): don't stop iteration on error 2024-04-24 00:00:10 +09:00
naskya 91901281cb
refactor (backend): replace pushNotification with the Rusty one 2024-04-24 00:00:10 +09:00
naskya f428fc7d9d
chore (backend-rs): push_notification -> send_push_notification 2024-04-24 00:00:10 +09:00
naskya b9e84113e2
chore (backend-rs): consider SendReadMessage setting in push notifications 2024-04-24 00:00:10 +09:00
naskya b66edf97b4
feat (backend-rs): implement push notifications 2024-04-24 00:00:09 +09:00
41 changed files with 1736 additions and 570 deletions

View File

@ -53,11 +53,9 @@ title.svg
/scripts
!/scripts/copy-assets.mjs
biome.json
COPYING
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Dockerfile
LICENSE
Procfile
README.md
SECURITY.md

View File

@ -12,6 +12,11 @@ workflow:
when: always
- if: $CI_MERGE_REQUEST_PROJECT_PATH == 'firefish/firefish'
when: always
- if: $CI_PROJECT_PATH != 'firefish/firefish'
changes:
paths:
- .gitlab-ci.yml
when: never
- when: never
cache:
@ -56,9 +61,11 @@ build_test:
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
- packages/**/*
- packages/backend/*
- packages/backend-rs/*
- packages/macro-rs/*
- packages/megalodon/*
- scripts/**/*
- locales/**/*
- package.json
- pnpm-lock.yaml
- Cargo.toml
@ -68,6 +75,33 @@ build_test:
- pnpm run build:debug
- pnpm run migrate
client_build_test:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
- packages/client/*
- packages/firefish-js/*
- packages/sw/*
- scripts/**/*
- locales/**/*
- package.json
- pnpm-lock.yaml
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
- packages/backend/*
- packages/backend-rs/*
- packages/macro-rs/*
- packages/megalodon/*
- Cargo.toml
- Cargo.lock
when: never
script:
- pnpm install --frozen-lockfile
- pnpm --filter 'firefish-js' --filter 'client' --filter 'sw' run build:debug
container_image_build:
stage: build
image: docker.io/debian:bookworm-slim
@ -90,8 +124,21 @@ container_image_build:
- apt-get install -y --no-install-recommends buildah ca-certificates fuse-overlayfs
- buildah login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
- export IMAGE_CACHE="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop/cache"
script:
- buildah build --isolation chroot --device /dev/fuse:rw --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add all --tag "${IMAGE_TAG}" --platform linux/amd64 .
- |-
buildah build \
--isolation chroot \
--device /dev/fuse:rw \
--security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
--cap-add all \
--platform linux/amd64 \
--layers \
--cache-to "${IMAGE_CACHE}" \
--cache-from "${IMAGE_CACHE}" \
--tag "${IMAGE_TAG}" \
.
- buildah inspect "${IMAGE_TAG}"
- buildah push "${IMAGE_TAG}"
@ -119,7 +166,7 @@ cargo_unit_test:
cargo_clippy:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_COMMIT_BRANCH == 'develop'
changes:
paths:
- packages/backend-rs/**/*
@ -129,6 +176,7 @@ cargo_clippy:
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
script:
- rustup component add clippy
- cargo clippy -- -D warnings
renovate:

View File

@ -26,10 +26,6 @@ RsaSignature2017 implementation by Transmute Industries Inc
License: MIT
https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE
Machine learning model for sensitive images by Infinite Red, Inc.
License: MIT
https://github.com/infinitered/nsfwjs/blob/master/LICENSE
Chiptune2.js by Simon Gündling
License: MIT
https://github.com/deskjet/chiptune2.js#license

553
Cargo.lock generated
View File

@ -179,9 +179,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "av1-grain"
@ -242,6 +242,7 @@ dependencies = [
"tracing-subscriber",
"url",
"urlencoding",
"web-push",
]
[[package]]
@ -259,6 +260,18 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.7"
@ -267,9 +280,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.0"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
@ -289,7 +302,7 @@ version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7"
dependencies = [
"base64 0.22.0",
"base64 0.22.1",
"blowfish",
"getrandom",
"subtle",
@ -307,6 +320,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "binstring"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e0d60973d9320722cb1206f412740e162a33b8547ea8d6be75d7cff237c7a85"
[[package]]
name = "bit_field"
version = "0.10.2"
@ -376,9 +395,9 @@ dependencies = [
[[package]]
name = "borsh"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6"
checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77"
dependencies = [
"borsh-derive",
"cfg_aliases",
@ -386,9 +405,9 @@ dependencies = [
[[package]]
name = "borsh-derive"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5"
checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d"
dependencies = [
"once_cell",
"proc-macro-crate",
@ -464,9 +483,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
[[package]]
name = "cc"
version = "1.0.95"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd"
dependencies = [
"jobserver",
"libc",
@ -520,6 +539,17 @@ dependencies = [
"inout",
]
[[package]]
name = "coarsetime"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b3839cf01bb7960114be3ccf2340f541b6d0c81f8690b007b2b39f750f7e5d"
dependencies = [
"libc",
"wasix",
"wasm-bindgen",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@ -545,6 +575,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b"
[[package]]
name = "const-oid"
version = "0.9.6"
@ -639,6 +675,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -649,6 +697,12 @@ dependencies = [
"typenum",
]
[[package]]
name = "ct-codecs"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df"
[[package]]
name = "ctor"
version = "0.2.8"
@ -708,17 +762,50 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "der"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4"
dependencies = [
"const-oid 0.6.2",
"der_derive",
]
[[package]]
name = "der"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
dependencies = [
"const-oid 0.9.6",
"pem-rfc7468 0.6.0",
"zeroize",
]
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid",
"pem-rfc7468",
"const-oid 0.9.6",
"pem-rfc7468 0.7.0",
"zeroize",
]
[[package]]
name = "der_derive"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aed3b3c608dc56cf36c45fe979d04eda51242e6703d8d0bb03426ef7c41db6a"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"synstructure",
]
[[package]]
name = "deranged"
version = "0.3.11"
@ -753,7 +840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"const-oid 0.9.6",
"crypto-common",
"subtle",
]
@ -764,6 +851,48 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der 0.7.9",
"digest",
"elliptic-curve",
"rfc6979",
"signature 2.2.0",
"spki 0.7.3",
]
[[package]]
name = "ece"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ea1d2f2cc974957a4e2575d8e5bb494549bab66338d6320c2789abcfff5746"
dependencies = [
"base64 0.21.7",
"byteorder",
"hex",
"hkdf",
"lazy_static",
"once_cell",
"openssl",
"serde",
"sha2",
"thiserror",
]
[[package]]
name = "ed25519-compact"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190"
dependencies = [
"ct-codecs",
"getrandom",
]
[[package]]
name = "either"
version = "1.11.0"
@ -773,6 +902,27 @@ dependencies = [
"serde",
]
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest",
"ff",
"generic-array",
"group",
"hkdf",
"pem-rfc7468 0.7.0",
"pkcs8 0.10.2",
"rand_core",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "emojis"
version = "0.6.2"
@ -851,9 +1001,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.0.2"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "fdeflate"
@ -864,6 +1014,16 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "ff"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"rand_core",
"subtle",
]
[[package]]
name = "finl_unicode"
version = "1.2.0"
@ -872,9 +1032,9 @@ checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
[[package]]
name = "flate2"
version = "1.0.29"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -1037,6 +1197,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
"zeroize",
]
[[package]]
@ -1046,8 +1207,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@ -1066,6 +1229,17 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"subtle",
]
[[package]]
name = "half"
version = "2.4.1"
@ -1087,9 +1261,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.14.3"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash 0.8.11",
"allocator-api2",
@ -1101,7 +1275,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.14.3",
"hashbrown 0.14.5",
]
[[package]]
@ -1149,6 +1323,30 @@ dependencies = [
"digest",
]
[[package]]
name = "hmac-sha1-compact"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9d405ec732fa3fcde87264e54a32a84956a377b3e3107de96e59b798c84a7"
[[package]]
name = "hmac-sha256"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735"
dependencies = [
"digest",
]
[[package]]
name = "hmac-sha512"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ce1f4656bae589a3fab938f9f09bf58645b7ed01a2c5f8a3c238e01a4ef78a"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.9"
@ -1248,7 +1446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
"hashbrown 0.14.5",
]
[[package]]
@ -1357,6 +1555,46 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jwt-simple"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357892bb32159d763abdea50733fadcb9a8e1c319a9aa77592db8555d05af83e"
dependencies = [
"anyhow",
"binstring",
"coarsetime",
"ct-codecs",
"ed25519-compact",
"hmac-sha1-compact",
"hmac-sha256",
"hmac-sha512",
"k256",
"p256",
"p384",
"rand",
"rsa 0.7.2",
"serde",
"serde_json",
"spki 0.6.0",
"thiserror",
"zeroize",
]
[[package]]
name = "k256"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b"
dependencies = [
"cfg-if",
"ecdsa",
"elliptic-curve",
"once_cell",
"sha2",
"signature 2.2.0",
]
[[package]]
name = "keccak"
version = "0.1.5"
@ -1383,9 +1621,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.153"
version = "0.2.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
[[package]]
name = "libfuzzer-sys"
@ -1733,9 +1971,9 @@ dependencies = [
[[package]]
name = "num-iter"
version = "0.1.44"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
@ -1756,9 +1994,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.18"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
@ -1882,6 +2120,30 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "p256"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]]
name = "p384"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]]
name = "parking"
version = "2.2.0"
@ -1928,6 +2190,35 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pem"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb"
dependencies = [
"base64 0.13.1",
"once_cell",
"regex",
]
[[package]]
name = "pem"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8"
dependencies = [
"base64 0.13.1",
]
[[package]]
name = "pem-rfc7468"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac"
dependencies = [
"base64ct",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@ -1993,15 +2284,37 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719"
dependencies = [
"der 0.6.1",
"pkcs8 0.9.0",
"spki 0.6.0",
"zeroize",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
"der 0.7.9",
"pkcs8 0.10.2",
"spki 0.7.3",
]
[[package]]
name = "pkcs8"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
dependencies = [
"der 0.6.1",
"spki 0.6.0",
]
[[package]]
@ -2010,8 +2323,8 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
"der 0.7.9",
"spki 0.7.3",
]
[[package]]
@ -2071,6 +2384,15 @@ dependencies = [
"yansi",
]
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "3.1.0"
@ -2353,6 +2675,16 @@ dependencies = [
"bytecheck",
]
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]]
name = "rgb"
version = "0.8.37"
@ -2419,31 +2751,52 @@ dependencies = [
[[package]]
name = "rmp-serde"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938a142ab806f18b88a97b0dea523d39e0fd730a064b035726adcfc58a8a5188"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]]
name = "rsa"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c"
dependencies = [
"byteorder",
"digest",
"num-bigint-dig",
"num-integer",
"num-iter",
"num-traits",
"pkcs1 0.4.1",
"pkcs8 0.9.0",
"rand_core",
"signature 1.6.4",
"smallvec",
"subtle",
"zeroize",
]
[[package]]
name = "rsa"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
dependencies = [
"const-oid",
"const-oid 0.9.6",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"pkcs1 0.7.5",
"pkcs8 0.10.2",
"rand_core",
"signature",
"spki",
"signature 2.2.0",
"spki 0.7.3",
"subtle",
"zeroize",
]
@ -2644,6 +2997,31 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der 0.7.9",
"generic-array",
"pkcs8 0.10.2",
"subtle",
"zeroize",
]
[[package]]
name = "sec1_decode"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6326ddc956378a0739200b2c30892dccaf198992dfd7323274690b9e188af23"
dependencies = [
"der 0.4.5",
"pem 0.8.3",
"thiserror",
]
[[package]]
name = "semver"
version = "1.0.22"
@ -2652,18 +3030,18 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
version = "1.0.198"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.198"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
dependencies = [
"proc-macro2",
"quote",
@ -2759,6 +3137,16 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
dependencies = [
"digest",
"rand_core",
]
[[package]]
name = "signature"
version = "2.2.0"
@ -2824,9 +3212,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.5.6"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
"libc",
"windows-sys 0.52.0",
@ -2847,6 +3235,16 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
dependencies = [
"base64ct",
"der 0.6.1",
]
[[package]]
name = "spki"
version = "0.7.3"
@ -2854,7 +3252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
"der 0.7.9",
]
[[package]]
@ -2999,7 +3397,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"rand",
"rsa",
"rsa 0.9.6",
"rust_decimal",
"serde",
"sha1",
@ -3169,6 +3567,18 @@ dependencies = [
"syn 2.0.60",
]
[[package]]
name = "synstructure"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"unicode-xid",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@ -3201,7 +3611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand 2.0.2",
"fastrand 2.1.0",
"rustix",
"windows-sys 0.52.0",
]
@ -3480,6 +3890,12 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-xid"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "unicode_categories"
version = "0.1.1"
@ -3577,6 +3993,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasix"
version = "0.12.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d"
dependencies = [
"wasi",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
@ -3631,6 +4056,28 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "web-push"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5c5c6deab45e8820b77c9c8ae168f1ded4595767c746584bb67b9100f2b71d"
dependencies = [
"async-trait",
"base64 0.13.1",
"chrono",
"ece",
"futures-lite",
"http",
"isahc",
"jwt-simple",
"log",
"pem 1.1.1",
"sec1_decode",
"serde",
"serde_derive",
"serde_json",
]
[[package]]
name = "webpki-roots"
version = "0.25.4"
@ -3858,18 +4305,18 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zerocopy"
version = "0.7.32"
version = "0.7.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
version = "0.7.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393"
dependencies = [
"proc-macro2",
"quote",

View File

@ -41,6 +41,7 @@ tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.0"
urlencoding = "2.1.3"
web-push = "0.10.1"
[profile.release]
lto = true

View File

@ -2,6 +2,8 @@
Breaking changes are indicated by the :warning: icon.
## Unreleased
- Adding `lang` to the response of `i` and the request parameter of `i/update`.
## v20240504

View File

@ -2244,3 +2244,5 @@ incorrectLanguageWarning: "It looks like your post is in {detected}, but you sel
noteEditHistory: "Post edit history"
slashQuote: "Chain quote"
foldNotification: "Group similar notifications"
mergeThreadInTimeline: "Merge multiple posts in the same thread in timelines"
mergeRenotesInTimeline: "Group multiple boosts of the same post"

View File

@ -2071,3 +2071,5 @@ noteEditHistory: "帖子编辑历史"
media: 媒体
slashQuote: "斜杠引用"
foldNotification: "将通知按同类型分组"
mergeThreadInTimeline: "将时间线内的连续回复合并成一串"
mergeRenotesInTimeline: "合并同一个帖子的转发"

View File

@ -45,6 +45,7 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
web-push = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }

View File

@ -1259,6 +1259,15 @@ export interface Users {
}
export function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise<void>
export function unwatchNote(watcherId: string, noteId: string): Promise<void>
export enum PushNotificationKind {
Generic = 'generic',
Chat = 'chat',
ReadAllChats = 'readAllChats',
ReadAllChatsInTheRoom = 'readAllChatsInTheRoom',
ReadNotifications = 'readNotifications',
ReadAllNotifications = 'readAllNotifications'
}
export function sendPushNotification(receiverUserId: string, kind: PushNotificationKind, content: any): Promise<void>
export function publishToChannelStream(channelId: string, userId: string): void
export enum ChatEvent {
Message = 'message',

View File

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE
@ -373,6 +373,8 @@ module.exports.Inbound = Inbound
module.exports.Outbound = Outbound
module.exports.watchNote = watchNote
module.exports.unwatchNote = unwatchNote
module.exports.PushNotificationKind = PushNotificationKind
module.exports.sendPushNotification = sendPushNotification
module.exports.publishToChannelStream = publishToChannelStream
module.exports.ChatEvent = ChatEvent
module.exports.publishToChatStream = publishToChatStream

View File

@ -1,4 +1,8 @@
use serde::{Deserialize, Serialize};
/// TODO: handle name collisions better
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")]
pub struct NoteLike {
pub file_ids: Vec<String>,

View File

@ -1,4 +1,5 @@
pub mod log;
pub mod nodeinfo;
pub mod note;
pub mod push_notification;
pub mod stream;

View File

@ -0,0 +1,227 @@
use crate::database::db_conn;
use crate::misc::get_note_summary::{get_note_summary, NoteLike};
use crate::misc::meta::fetch_meta;
use crate::model::entity::sw_subscription;
use once_cell::sync::OnceCell;
use sea_orm::{prelude::*, DbErr};
use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushError, WebPushMessageBuilder,
};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Database error: {0}")]
DbErr(#[from] DbErr),
#[error("Web Push error: {0}")]
WebPushErr(#[from] WebPushError),
#[error("Failed to (de)serialize an object: {0}")]
SerializeErr(#[from] serde_json::Error),
#[error("Invalid content: {0}")]
InvalidContentErr(String),
}
static CLIENT: OnceCell<IsahcWebPushClient> = OnceCell::new();
fn get_client() -> Result<IsahcWebPushClient, WebPushError> {
CLIENT.get_or_try_init(IsahcWebPushClient::new).cloned()
}
#[derive(strum::Display, PartialEq)]
#[crate::export(string_enum = "camelCase")]
pub enum PushNotificationKind {
#[strum(serialize = "notification")]
Generic,
#[strum(serialize = "unreadMessagingMessage")]
Chat,
#[strum(serialize = "readAllMessagingMessages")]
ReadAllChats,
#[strum(serialize = "readAllMessagingMessagesOfARoom")]
ReadAllChatsInTheRoom,
#[strum(serialize = "readNotifications")]
ReadNotifications,
#[strum(serialize = "readAllNotifications")]
ReadAllNotifications,
}
fn compact_content(
kind: &PushNotificationKind,
mut content: serde_json::Value,
) -> Result<serde_json::Value, Error> {
if kind != &PushNotificationKind::Generic {
return Ok(content);
}
if !content.is_object() {
return Err(Error::InvalidContentErr("not a JSON object".to_string()));
}
let object = content.as_object_mut().unwrap();
if !object.contains_key("note") {
return Ok(content);
}
let mut note = if object.contains_key("type") && object.get("type").unwrap() == "renote" {
object
.get("note")
.unwrap()
.get("renote")
.ok_or(Error::InvalidContentErr(
"renote object is missing".to_string(),
))?
} else {
object.get("note").unwrap()
}
.clone();
if !note.is_object() {
return Err(Error::InvalidContentErr(
"(re)note is not an object".to_string(),
));
}
let note_like: NoteLike = serde_json::from_value(note.clone())?;
let text = get_note_summary(note_like);
let note_object = note.as_object_mut().unwrap();
note_object.remove("reply");
note_object.remove("renote");
note_object.remove("user");
note_object.insert("text".to_string(), text.into());
object.insert("note".to_string(), note);
Ok(serde_json::from_value(Json::Object(object.clone()))?)
}
async fn handle_web_push_failure(
db: &DatabaseConnection,
err: WebPushError,
subscription_id: &str,
error_message: &str,
) -> Result<(), DbErr> {
match err {
WebPushError::BadRequest(_)
| WebPushError::ServerError(_)
| WebPushError::InvalidUri
| WebPushError::EndpointNotValid
| WebPushError::EndpointNotFound
| WebPushError::TlsError
| WebPushError::SslError
| WebPushError::InvalidPackageName
| WebPushError::MissingCryptoKeys
| WebPushError::InvalidCryptoKeys
| WebPushError::InvalidResponse => {
sw_subscription::Entity::delete_by_id(subscription_id)
.exec(db)
.await?;
tracing::info!("{}; {} was unsubscribed", error_message, subscription_id);
tracing::debug!("reason: {:#?}", err);
}
_ => {
tracing::warn!("{}; subscription id: {}", error_message, subscription_id);
tracing::info!("reason: {:#?}", err);
}
};
Ok(())
}
#[crate::export]
pub async fn send_push_notification(
receiver_user_id: &str,
kind: PushNotificationKind,
content: &serde_json::Value,
) -> Result<(), Error> {
let meta = fetch_meta(true).await?;
if !meta.enable_service_worker || meta.sw_public_key.is_none() || meta.sw_private_key.is_none()
{
return Ok(());
}
let db = db_conn().await?;
let signature_builder = VapidSignatureBuilder::from_base64_no_sub(
meta.sw_private_key.unwrap().as_str(),
web_push::URL_SAFE_NO_PAD,
)?;
let subscriptions = sw_subscription::Entity::find()
.filter(sw_subscription::Column::UserId.eq(receiver_user_id))
.all(db)
.await?;
let payload = format!(
"{{\"type\":\"{}\",\"userId\":\"{}\",\"dateTime\":{},\"body\":{}}}",
kind,
receiver_user_id,
chrono::Utc::now().timestamp_millis(),
serde_json::to_string(&compact_content(&kind, content.clone())?)?
);
tracing::trace!("payload: {:#?}", payload);
for subscription in subscriptions.iter() {
if !subscription.send_read_message
&& [
PushNotificationKind::ReadAllChats,
PushNotificationKind::ReadAllChatsInTheRoom,
PushNotificationKind::ReadAllNotifications,
PushNotificationKind::ReadNotifications,
]
.contains(&kind)
{
continue;
}
let subscription_info = SubscriptionInfo {
endpoint: subscription.endpoint.to_owned(),
keys: SubscriptionKeys {
// convert standard base64 into base64url
// https://en.wikipedia.org/wiki/Base64#Variants_summary_table
p256dh: subscription
.publickey
.replace('+', "-")
.replace('/', "_")
.to_owned(),
auth: subscription
.auth
.replace('+', "-")
.replace('/', "_")
.to_owned(),
},
};
let signature = signature_builder
.clone()
.add_sub_info(&subscription_info)
.build();
if let Err(err) = signature {
handle_web_push_failure(db, err, &subscription.id, "failed to build a signature")
.await?;
continue;
}
let mut message_builder = WebPushMessageBuilder::new(&subscription_info);
message_builder.set_ttl(1000);
message_builder.set_payload(ContentEncoding::Aes128Gcm, payload.as_bytes());
message_builder.set_vapid_signature(signature.unwrap());
let message = message_builder.build();
if let Err(err) = message {
handle_web_push_failure(db, err, &subscription.id, "failed to build a payload").await?;
continue;
}
if let Err(err) = get_client()?.send(message.unwrap()).await {
handle_web_push_failure(db, err, &subscription.id, "failed to send").await?;
continue;
}
tracing::debug!("success; subscription id: {}", subscription.id);
}
Ok(())
}

View File

@ -59,11 +59,11 @@
"form-data": "^4.0.0",
"got": "14.2.1",
"gunzip-maybe": "^1.4.2",
"happy-dom": "^14.7.1",
"hpagent": "1.2.0",
"ioredis": "5.4.1",
"ip-cidr": "4.0.0",
"is-svg": "5.0.0",
"jsdom": "24.0.0",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
@ -119,7 +119,6 @@
"typeorm": "0.3.20",
"ulid": "2.3.0",
"uuid": "9.0.1",
"web-push": "3.6.7",
"websocket": "1.0.34",
"xev": "3.0.2"
},
@ -131,6 +130,7 @@
"@types/content-disposition": "^0.5.8",
"@types/escape-regexp": "0.0.3",
"@types/fluent-ffmpeg": "2.1.24",
"@types/jsdom": "21.1.6",
"@types/jsonld": "1.5.13",
"@types/jsrsasign": "10.5.13",
"@types/katex": "0.16.7",

View File

@ -1,21 +1,17 @@
import { type HTMLElement, Window } from "happy-dom";
import { JSDOM } from "jsdom";
import type * as mfm from "mfm-js";
import katex from "katex";
import { config } from "@/config.js";
import { intersperse } from "@/prelude/array.js";
import type { IMentionedRemoteUsers } from "@/models/entities/note.js";
function toMathMl(code: string, displayMode: boolean): HTMLElement | null {
const { window } = new Window();
const document = window.document;
document.body.innerHTML = katex.renderToString(code, {
function toMathMl(code: string, displayMode: boolean): MathMLElement | null {
const rendered = katex.renderToString(code, {
throwOnError: false,
output: "mathml",
displayMode,
});
return document.querySelector("math");
return JSDOM.fragment(rendered).querySelector("math");
}
export function toHtml(
@ -26,7 +22,7 @@ export function toHtml(
return null;
}
const { window } = new Window();
const { window } = new JSDOM("");
const doc = window.document;

View File

@ -3,10 +3,11 @@ import {
publishToChatStream,
publishToGroupChatStream,
publishToChatIndexStream,
sendPushNotification,
ChatEvent,
ChatIndexEvent,
PushNotificationKind,
} from "backend-rs";
import { pushNotification } from "@/services/push-notification.js";
import type { User, IRemoteUser } from "@/models/entities/user.js";
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
import { MessagingMessages, UserGroupJoinings, Users } from "@/models/index.js";
@ -62,20 +63,19 @@ export async function readUserMessagingMessage(
if (!(await Users.getHasUnreadMessagingMessage(userId))) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, "readAllMessagingMessages");
pushNotification(userId, "readAllMessagingMessages", undefined);
sendPushNotification(userId, PushNotificationKind.ReadAllChats, {});
} else {
// そのユーザーとのメッセージで未読がなければイベント発行
const count = await MessagingMessages.count({
const hasUnread = await MessagingMessages.exists({
where: {
userId: otherpartyId,
recipientId: userId,
isRead: false,
},
take: 1,
});
if (!count) {
pushNotification(userId, "readAllMessagingMessagesOfARoom", {
if (!hasUnread) {
sendPushNotification(userId, PushNotificationKind.ReadAllChatsInTheRoom, {
userId: otherpartyId,
});
}
@ -137,10 +137,10 @@ export async function readGroupMessagingMessage(
if (!(await Users.getHasUnreadMessagingMessage(userId))) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, "readAllMessagingMessages");
pushNotification(userId, "readAllMessagingMessages", undefined);
sendPushNotification(userId, PushNotificationKind.ReadAllChats, {});
} else {
// そのグループにおいて未読がなければイベント発行
const unreadExist = await MessagingMessages.createQueryBuilder("message")
const hasUnread = await MessagingMessages.createQueryBuilder("message")
.where("message.groupId = :groupId", { groupId: groupId })
.andWhere("message.userId != :userId", { userId: userId })
.andWhere("NOT (:userId = ANY(message.reads))", { userId: userId })
@ -150,8 +150,10 @@ export async function readGroupMessagingMessage(
.getOne()
.then((x) => x != null);
if (!unreadExist) {
pushNotification(userId, "readAllMessagingMessagesOfARoom", { groupId });
if (!hasUnread) {
sendPushNotification(userId, PushNotificationKind.ReadAllChatsInTheRoom, {
groupId,
});
}
}
}

View File

@ -1,6 +1,6 @@
import { In } from "typeorm";
import { publishMainStream } from "@/services/stream.js";
import { pushNotification } from "@/services/push-notification.js";
import { sendPushNotification, PushNotificationKind } from "backend-rs";
import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js";
import { Notifications, Users } from "@/models/index.js";
@ -47,7 +47,11 @@ export async function readNotificationByQuery(
function postReadAllNotifications(userId: User["id"]) {
publishMainStream(userId, "readAllNotifications");
return pushNotification(userId, "readAllNotifications", undefined);
return sendPushNotification(
userId,
PushNotificationKind.ReadAllNotifications,
{},
);
}
function postReadNotifications(
@ -55,5 +59,7 @@ function postReadNotifications(
notificationIds: Notification["id"][],
) {
publishMainStream(userId, "readNotifications", notificationIds);
return pushNotification(userId, "readNotifications", { notificationIds });
return sendPushNotification(userId, PushNotificationKind.ReadNotifications, {
notificationIds,
});
}

View File

@ -1,5 +1,5 @@
import { publishMainStream } from "@/services/stream.js";
import { pushNotification } from "@/services/push-notification.js";
import { sendPushNotification, PushNotificationKind } from "backend-rs";
import { Notifications } from "@/models/index.js";
import define from "@/server/api/define.js";
@ -17,7 +17,7 @@ export const paramDef = {
required: [],
} as const;
export default define(meta, paramDef, async (ps, user) => {
export default define(meta, paramDef, async (_, user) => {
// Update documents
await Notifications.update(
{
@ -31,5 +31,5 @@ export default define(meta, paramDef, async (ps, user) => {
// 全ての通知を読みましたよというイベントを発行
publishMainStream(user.id, "readAllNotifications");
pushNotification(user.id, "readAllNotifications", undefined);
sendPushNotification(user.id, PushNotificationKind.ReadAllNotifications, {});
});

View File

@ -1,5 +1,4 @@
import { publishMainStream } from "@/services/stream.js";
import { pushNotification } from "@/services/push-notification.js";
import {
Notifications,
Mutings,
@ -8,7 +7,12 @@ import {
Users,
Followings,
} from "@/models/index.js";
import { genId, isSilencedServer } from "backend-rs";
import {
genId,
isSilencedServer,
sendPushNotification,
PushNotificationKind,
} from "backend-rs";
import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js";
import { sendEmailNotification } from "./send-email-notification.js";
@ -81,7 +85,7 @@ export async function createNotification(
if (fresh == null) return; // 既に削除されているかもしれない
// We execute this before, because the server side "read" check doesnt work well with push notifications, the app and service worker will decide themself
// when it is best to show push notifications
pushNotification(notifieeId, "notification", packed);
sendPushNotification(notifieeId, PushNotificationKind.Generic, packed);
if (fresh.isRead) return;
//#region ただしミュートしているユーザーからの通知なら無視

View File

@ -1,8 +1,8 @@
import { URL } from "node:url";
import { Window } from "happy-dom";
import { type DOMWindow, JSDOM } from "jsdom";
import fetch from "node-fetch";
import tinycolor from "tinycolor2";
import { getJson, getAgentByUrl } from "@/misc/fetch.js";
import { getJson, getHtml, getAgentByUrl } from "@/misc/fetch.js";
import {
type Instance,
MAX_LENGTH_INSTANCE,
@ -112,15 +112,13 @@ export async function fetchInstanceMetadata(
}
}
async function fetchDom(instance: Instance): Promise<Window["document"]> {
async function fetchDom(instance: Instance): Promise<DOMWindow["document"]> {
logger.info(`Fetching HTML of ${instance.host} ...`);
const window = new Window({
url: `https://${instance.host}`,
});
const doc = window.document;
const html = await getHtml(`https://${instance.host}`);
const { window } = new JSDOM(html);
return doc;
return window.document;
}
async function fetchManifest(
@ -137,7 +135,7 @@ async function fetchManifest(
async function fetchFaviconUrl(
instance: Instance,
doc: Window["document"] | null,
doc: DOMWindow["document"] | null,
): Promise<string | null> {
const url = `https://${instance.host}`;
@ -169,7 +167,7 @@ async function fetchFaviconUrl(
async function fetchIconUrl(
instance: Instance,
doc: Window["document"] | null,
doc: DOMWindow["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | null> {
if (manifest?.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
@ -219,9 +217,9 @@ async function getThemeColor(
async function getSiteName(
info: Nodeinfo | null,
doc: Window["document"] | null,
doc: DOMWindow["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | undefined | null> {
): Promise<string | null> {
if (info?.metadata) {
if (info.metadata.nodeName || info.metadata.name) {
return info.metadata.nodeName || info.metadata.name;
@ -247,7 +245,7 @@ async function getSiteName(
async function getDescription(
info: Nodeinfo | null,
doc: Window["document"] | null,
doc: DOMWindow["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | null> {
if (info?.metadata) {

View File

@ -1,12 +1,11 @@
import { Window } from "happy-dom";
import type { HTMLAnchorElement, HTMLLinkElement } from "happy-dom";
import { JSDOM } from "jsdom";
import { config } from "@/config.js";
import { getHtml } from "@/misc/fetch.js";
async function getRelMeLinks(url: string): Promise<string[]> {
try {
const dom = new Window({
url: url,
});
const html = await getHtml(url);
const dom = new JSDOM(html);
const allLinks = [...dom.window.document.querySelectorAll("a, link")];
const relMeLinks = allLinks
.filter((a) => {

View File

@ -9,16 +9,17 @@ import {
} from "@/models/index.js";
import {
genId,
sendPushNotification,
publishToChatStream,
publishToGroupChatStream,
publishToChatIndexStream,
toPuny,
ChatEvent,
ChatIndexEvent,
PushNotificationKind,
} from "backend-rs";
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
import { publishMainStream } from "@/services/stream.js";
import { pushNotification } from "@/services/push-notification.js";
import { Not } from "typeorm";
import type { Note } from "@/models/entities/note.js";
import renderNote from "@/remote/activitypub/renderer/note.js";
@ -118,7 +119,11 @@ export async function createMessage(
//#endregion
publishMainStream(recipientUser.id, "unreadMessagingMessage", messageObj);
pushNotification(recipientUser.id, "unreadMessagingMessage", messageObj);
sendPushNotification(
recipientUser.id,
PushNotificationKind.Chat,
messageObj,
);
} else if (recipientGroup) {
const joinings = await UserGroupJoinings.findBy({
userGroupId: recipientGroup.id,
@ -127,7 +132,11 @@ export async function createMessage(
for (const joining of joinings) {
if (freshMessage.reads.includes(joining.userId)) return; // 既読
publishMainStream(joining.userId, "unreadMessagingMessage", messageObj);
pushNotification(joining.userId, "unreadMessagingMessage", messageObj);
sendPushNotification(
joining.userId,
PushNotificationKind.Chat,
messageObj,
);
}
}
}, 2000);

View File

@ -1,115 +0,0 @@
import push from "web-push";
import { config } from "@/config.js";
import { SwSubscriptions } from "@/models/index.js";
import { fetchMeta, getNoteSummary } from "backend-rs";
import type { Packed } from "@/misc/schema.js";
// Defined also packages/sw/types.ts#L14-L21
type pushNotificationsTypes = {
notification: Packed<"Notification">;
unreadMessagingMessage: Packed<"MessagingMessage">;
readNotifications: { notificationIds: string[] };
readAllNotifications: undefined;
readAllMessagingMessages: undefined;
readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string };
};
// プッシュメッセージサーバーには文字数制限があるため、内容を削減します
function truncateNotification(notification: Packed<"Notification">): any {
if (notification.note != null) {
return {
...notification,
note: {
...notification.note,
// replace the text with summary
text: getNoteSummary(
notification.type === "renote" && notification.note.renote != null
? notification.note.renote
: notification.note,
),
cw: undefined,
reply: undefined,
renote: undefined,
user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる
},
};
}
return notification;
}
export async function pushNotification<T extends keyof pushNotificationsTypes>(
userId: string,
type: T,
body: pushNotificationsTypes[T],
) {
const meta = await fetchMeta(true);
if (
!meta.enableServiceWorker ||
meta.swPublicKey == null ||
meta.swPrivateKey == null
)
return;
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(config.url, meta.swPublicKey, meta.swPrivateKey);
// Fetch
const subscriptions = await SwSubscriptions.findBy({
userId: userId,
});
for (const subscription of subscriptions) {
if (
[
"readNotifications",
"readAllNotifications",
"readAllMessagingMessages",
"readAllMessagingMessagesOfARoom",
].includes(type) &&
!subscription.sendReadMessage
)
continue;
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {
auth: subscription.auth,
p256dh: subscription.publickey,
},
};
push
.sendNotification(
pushSubscription,
JSON.stringify({
type,
body:
type === "notification"
? truncateNotification(body as Packed<"Notification">)
: body,
userId,
dateTime: Date.now(),
}),
{
proxy: config.proxy,
},
)
.catch((err: any) => {
//swLogger.info(err.statusCode);
//swLogger.info(err.headers);
//swLogger.info(err.body);
if (err.statusCode === 410) {
SwSubscriptions.delete({
userId: userId,
endpoint: subscription.endpoint,
auth: subscription.auth,
publickey: subscription.publickey,
});
}
});
}
}

View File

@ -40,7 +40,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import type { entities } from "firefish-js";
import PhotoSwipeLightbox from "photoswipe/lightbox";
import PhotoSwipe from "photoswipe";
@ -207,9 +207,9 @@ const isModule = (file: entities.DriveFile): boolean => {
);
};
const previewableCount = props.mediaList.filter((media) =>
previewable(media),
).length;
const previewableCount = computed(
() => props.mediaList.filter((media) => previewable(media)).length,
);
</script>
<style lang="scss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<div
v-if="!muted.muted"
v-show="!isDeleted"
v-show="!isDeleted && renotes?.length !== 0"
:id="appearNote.historyId || appearNote.id"
ref="el"
v-hotkey="keymap"
@ -10,13 +10,20 @@
:aria-label="accessibleLabel"
class="tkcbzcuz note-container"
:tabindex="!isDeleted ? '-1' : undefined"
:class="{ renote: isRenote }"
:class="{ renote: isRenote || (renotesSliced && renotesSliced.length > 0) }"
>
<MkNoteSub
v-if="appearNote.reply && !detailedView && !collapsedReply"
v-if="appearNote.reply && !detailedView && !collapsedReply && !parents"
:note="appearNote.reply"
class="reply-to"
/>
<MkNoteSub
v-else-if="!detailedView && !collapsedReply && parents"
v-for="n of parents"
:key="n.id"
:note="n"
class="reply-to"
/>
<div
v-if="!detailedView"
class="note-context"
@ -41,35 +48,6 @@
<div v-if="pinned" class="info">
<i :class="icon('ph-push-pin')"></i>{{ i18n.ts.pinnedNote }}
</div>
<div v-if="isRenote" class="renote">
<i :class="icon('ph-rocket-launch')"></i>
<I18n :src="i18n.ts.renotedBy" tag="span">
<template #user>
<MkA
v-user-preview="note.userId"
class="name"
:to="userPage(note.user)"
@click.stop
>
<MkUserName :user="note.user" />
</MkA>
</template>
</I18n>
<div class="info">
<button
ref="renoteTime"
class="_button time"
@click.stop="showRenoteMenu()"
>
<i
v-if="isMyRenote"
:class="icon('ph-dots-three-outline dropdownIcon')"
></i>
<MkTime :time="note.createdAt" />
</button>
<MkVisibility :note="note" />
</div>
</div>
<div v-if="collapsedReply && appearNote.reply" class="info">
<MkAvatar class="avatar" :user="appearNote.reply.user" />
<MkUserName
@ -85,6 +63,71 @@
:custom-emojis="note.emojis"
/>
</div>
<div v-if="isRenote || (renotesSliced && renotesSliced.length > 0)" class="renote">
<i :class="icon('ph-rocket-launch')"></i>
<I18n
v-if="renotesSliced == null"
:src="i18n.ts.renotedBy"
tag="span"
>
<template #user>
<MkAvatar class="avatar" :user="note.user" />
<MkA
v-user-preview="note.userId"
class="name"
:to="userPage(note.user)"
@click.stop
>
<MkUserName :user="note.user" />
</MkA>
</template>
</I18n>
<I18n
v-else
:src="i18n.ts.renotedBy"
tag="span"
>
<template #user>
<template
v-for="(renote, index) in renotesSliced"
>
<MkAvatar
class="avatar"
:user="renote.user"
/>
<MkA
v-user-preview="renote.userId"
class="name"
:to="userPage(renote.user)"
@click.stop
>
<MkUserName :user="renote.user" />
</MkA>
{{
index !== renotesSliced.length - 1
? ", "
: renotesSliced.length < renotes!.length
? "..."
: ""
}}
</template>
</template>
</I18n>
<div class="info">
<button
ref="renoteTime"
class="_button time"
@click.stop="showRenoteMenu()"
>
<i
v-if="isMyNote"
:class="icon('ph-dots-three-outline dropdownIcon')"
></i>
<MkTime :time="note.createdAt" />
</button>
<MkVisibility :note="note" />
</div>
</div>
</div>
<article
class="article"
@ -279,7 +322,7 @@
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, ref } from "vue";
import { computed, inject, onMounted, ref, watch } from "vue";
import type { Ref } from "vue";
import type { entities } from "firefish-js";
import MkSubNoteContent from "./MkSubNoteContent.vue";
@ -310,17 +353,13 @@ import { notePage } from "@/filters/note";
import { deepClone } from "@/scripts/clone";
import { getNoteSummary } from "@/scripts/get-note-summary";
import icon from "@/scripts/icon";
import type { NoteTranslation } from "@/types/note";
const router = useRouter();
type NoteType = entities.Note & {
_featuredId_?: string;
_prId_?: string;
};
import type { NoteTranslation, NoteType } from "@/types/note";
import { isRenote as _isRenote, isDeleted as _isDeleted } from "@/scripts/note";
const props = defineProps<{
note: NoteType;
parents?: NoteType[];
renotes?: entities.Note[];
pinned?: boolean;
detailedView?: boolean;
collapsedReply?: boolean;
@ -329,37 +368,20 @@ const props = defineProps<{
isLongJudger?: (note: entities.Note) => boolean;
}>();
//#region Constants
const router = useRouter();
const inChannel = inject("inChannel", null);
const note = ref(deepClone(props.note));
const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason;
if (what === "reply") return i18n.ts.userSaysSomethingReasonReply;
if (what === "renote") return i18n.ts.userSaysSomethingReasonRenote;
if (what === "quote") return i18n.ts.userSaysSomethingReasonQuote;
// I don't think here is reachable, but just in case
return i18n.ts.userSaysSomething;
const keymap = {
r: () => reply(true),
"e|a|plus": () => react(true),
q: () => renoteButton.value!.renote(true),
"up|k": focusBefore,
"down|j": focusAfter,
esc: blur,
"m|o": () => menu(true),
// FIXME: What's this?
// s: () => showContent.value !== showContent.value,
};
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}
note.value = result;
});
}
const isRenote =
note.value.renote != null &&
note.value.text == null &&
note.value.fileIds.length === 0 &&
note.value.poll == null;
const el = ref<HTMLElement | null>(null);
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>();
@ -367,42 +389,179 @@ const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
const renoteTime = ref<HTMLElement>();
const reactButton = ref<HTMLElement | null>(null);
const appearNote = computed(() =>
isRenote ? (note.value.renote as NoteType) : note.value,
);
const isMyRenote = isSignedIn(me) && me.id === note.value.userId;
// const showContent = ref(false);
const isDeleted = ref(false);
const muted = ref(
getWordSoftMute(
note.value,
me?.id,
defaultStore.state.mutedWords,
defaultStore.state.mutedLangs,
),
);
const translation = ref<NoteTranslation | null>(null);
const translating = ref(false);
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const enableEmojiReactions = defaultStore.reactiveState.enableEmojiReactions;
const expandOnNoteClick = defaultStore.reactiveState.expandOnNoteClick;
const lang = localStorage.getItem("lang");
const translateLang = localStorage.getItem("translateLang");
const targetLang = (translateLang || lang || navigator.language)?.slice(0, 2);
const currentClipPage = inject<Ref<entities.Clip> | null>(
"currentClipPage",
null,
);
//#endregion
const isForeignLanguage: boolean =
defaultStore.state.detectPostLanguage &&
appearNote.value.text != null &&
(() => {
const postLang = detectLanguage(appearNote.value.text);
return postLang !== "" && postLang !== targetLang;
})();
//#region Variables bound to Notes
let capture: ReturnType<typeof useNoteCapture> | undefined;
const note = ref(deepClone(props.note));
const postIsExpanded = ref(false);
const translation = ref<NoteTranslation | null>(null);
const translating = ref(false);
const isDeleted = ref(false);
const renotes = ref(props.renotes?.filter((rn) => !_isDeleted(rn.id)));
//#endregion
//#region computed
const renotesSliced = computed(() => renotes.value?.slice(0, 5));
const isRenote = computed(() => _isRenote(note.value));
const appearNote = computed(() =>
isRenote.value ? (note.value.renote as NoteType) : note.value,
);
const isMyNote = computed(
() => isSignedIn(me) && me.id === note.value.userId && props.renotes == null,
);
const muted = computed(() =>
getWordSoftMute(
note.value,
me?.id,
defaultStore.reactiveState.mutedWords.value,
defaultStore.reactiveState.mutedLangs.value,
),
);
const isForeignLanguage = computed(
() =>
defaultStore.state.detectPostLanguage &&
appearNote.value.text != null &&
(() => {
const postLang = detectLanguage(appearNote.value.text);
return postLang !== "" && postLang !== targetLang;
})(),
);
const reactionCount = computed(() =>
Object.values(appearNote.value.reactions).reduce(
(partialSum, val) => partialSum + val,
0,
),
);
const accessibleLabel = computed(() => {
let label = `${appearNote.value.user.username}; `;
if (appearNote.value.renote) {
label += `${i18n.ts.renoted} ${appearNote.value.renote.user.username}; `;
if (appearNote.value.renote.cw) {
label += `${i18n.ts.cw}: ${appearNote.value.renote.cw}; `;
if (postIsExpanded.value) {
label += `${appearNote.value.renote.text}; `;
}
} else {
label += `${appearNote.value.renote.text}; `;
}
} else {
if (appearNote.value.cw) {
label += `${i18n.ts.cw}: ${appearNote.value.cw}; `;
if (postIsExpanded.value) {
label += `${appearNote.value.text}; `;
}
} else {
label += `${appearNote.value.text}; `;
}
}
const date = new Date(appearNote.value.createdAt);
label += `${date.toLocaleTimeString()}`;
return label;
});
//#endregion
async function pluginInit(newNote: NoteType) {
// plugin
if (noteViewInterruptors.length > 0) {
let result = deepClone(newNote);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}
note.value = result;
}
}
function recalculateRenotes() {
renotes.value = props.renotes?.filter((rn) => !_isDeleted(rn.id));
}
async function init(newNote: NoteType, first = false) {
if (!first) {
// plugin
if (noteViewInterruptors.length > 0) {
await pluginInit(newNote);
} else {
note.value = deepClone(newNote);
}
}
translation.value = null;
translating.value = false;
postIsExpanded.value = false;
isDeleted.value = _isDeleted(note.value.id);
if (appearNote.value.historyId == null) {
capture?.close();
capture = useNoteCapture({
rootEl: el,
note: appearNote,
isDeletedRef: isDeleted,
});
if (isRenote.value === true) {
useNoteCapture({
rootEl: el,
note,
isDeletedRef: isDeleted,
});
}
if (props.renotes) {
const renoteDeletedTrigger = ref(false);
for (const renote of props.renotes) {
useNoteCapture({
rootEl: el,
note: ref(renote),
isDeletedRef: renoteDeletedTrigger,
});
}
watch(renoteDeletedTrigger, recalculateRenotes);
}
}
}
init(props.note, true);
onMounted(() => {
pluginInit(note.value);
});
watch(isDeleted, () => {
if (isDeleted.value === true) {
if (props.parents && props.parents.length > 0) {
let noteTakePlace: NoteType | null = null;
while (noteTakePlace == null || _isDeleted(noteTakePlace.id)) {
if (props.parents.length === 0) {
return;
}
noteTakePlace = props.parents[props.parents.length - 1];
props.parents.pop();
}
noteTakePlace.repliesCount -= 1;
init(noteTakePlace);
isDeleted.value = false;
}
}
});
watch(
() => props.note.id,
(o, n) => {
if (o !== n && _isDeleted(note.value.id) !== true) {
init(props.note);
}
},
);
watch(() => props.renotes?.length, recalculateRenotes);
async function translate_(noteId: string, targetLang: string) {
return await os.api("notes/translate", {
@ -431,24 +590,14 @@ async function translate() {
translating.value = false;
}
const keymap = {
r: () => reply(true),
"e|a|plus": () => react(true),
q: () => renoteButton.value!.renote(true),
"up|k": focusBefore,
"down|j": focusAfter,
esc: blur,
"m|o": () => menu(true),
// FIXME: What's this?
// s: () => showContent.value !== showContent.value,
};
function softMuteReasonI18nSrc(what?: string) {
if (what === "note") return i18n.ts.userSaysSomethingReason;
if (what === "reply") return i18n.ts.userSaysSomethingReasonReply;
if (what === "renote") return i18n.ts.userSaysSomethingReasonRenote;
if (what === "quote") return i18n.ts.userSaysSomethingReasonQuote;
if (appearNote.value.historyId == null) {
useNoteCapture({
rootEl: el,
note: appearNote,
isDeletedRef: isDeleted,
});
// I don't think here is reachable, but just in case
return i18n.ts.userSaysSomething;
}
function reply(_viaKeyboard = false): void {
@ -489,11 +638,6 @@ function undoReact(note: NoteType): void {
});
}
const currentClipPage = inject<Ref<entities.Clip> | null>(
"currentClipPage",
null,
);
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement): boolean => {
if (el.tagName === "A") return true;
@ -582,7 +726,7 @@ function menu(viaKeyboard = false): void {
}
function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
if (!isMyNote.value) return;
os.popupMenu(
[
{
@ -643,39 +787,10 @@ function readPromo() {
isDeleted.value = true;
}
const postIsExpanded = ref(false);
function setPostExpanded(val: boolean) {
postIsExpanded.value = val;
}
const accessibleLabel = computed(() => {
let label = `${appearNote.value.user.username}; `;
if (appearNote.value.renote) {
label += `${i18n.ts.renoted} ${appearNote.value.renote.user.username}; `;
if (appearNote.value.renote.cw) {
label += `${i18n.ts.cw}: ${appearNote.value.renote.cw}; `;
if (postIsExpanded.value) {
label += `${appearNote.value.renote.text}; `;
}
} else {
label += `${appearNote.value.renote.text}; `;
}
} else {
if (appearNote.value.cw) {
label += `${i18n.ts.cw}: ${appearNote.value.cw}; `;
if (postIsExpanded.value) {
label += `${appearNote.value.text}; `;
}
} else {
label += `${appearNote.value.text}; `;
}
}
const date = new Date(appearNote.value.createdAt);
label += `${date.toLocaleTimeString()}`;
return label;
});
defineExpose({
focus,
blur,
@ -749,6 +864,7 @@ defineExpose({
position: relative;
padding: 0 32px 0 32px;
display: flex;
flex-wrap: wrap;
z-index: 1;
&:first-child {
margin-top: 20px;
@ -801,6 +917,16 @@ defineExpose({
margin-right: 4px;
}
.avatar {
width: 1.2em;
height: 1.2em;
border-radius: 2em;
overflow: hidden;
margin-right: 0.4em;
background: var(--panelHighlight);
transform: translateY(-4px);
}
> span {
overflow: hidden;
flex-shrink: 1;

View File

@ -48,8 +48,6 @@
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { entities } from "firefish-js";
import { defaultStore } from "@/store";
import MkVisibility from "@/components/MkVisibility.vue";
@ -66,18 +64,16 @@ const props = defineProps<{
canOpenServerInfo?: boolean;
}>();
const note = ref(props.note);
const showTicker =
defaultStore.state.instanceTicker === "always" ||
(defaultStore.state.instanceTicker === "remote" && note.value.user.instance);
(defaultStore.state.instanceTicker === "remote" && props.note.user.instance);
function openServerInfo() {
if (!props.canOpenServerInfo || !defaultStore.state.openServerInfo) return;
const instanceInfoUrl =
note.value.user.host == null
props.note.user.host == null
? "/about"
: `/instance-info/${note.value.user.host}`;
: `/instance-info/${props.note.user.host}`;
pageWindow(instanceInfoUrl);
}
</script>

View File

@ -1,5 +1,10 @@
<template>
<div v-size="{ min: [350, 500] }" class="yohlumlk">
<div
v-show="!deleted"
v-size="{ min: [350, 500] }"
class="yohlumlk"
ref="el"
>
<MkAvatar class="avatar" :user="note.user" />
<div class="main">
<XNoteHeader class="header" :note="note" :mini="true" />
@ -14,11 +19,40 @@
import type { entities } from "firefish-js";
import XNoteHeader from "@/components/MkNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
import { computed, ref, watch } from "vue";
import { deepClone } from "@/scripts/clone";
import { useNoteCapture } from "@/scripts/use-note-capture";
import { isDeleted } from "@/scripts/note";
defineProps<{
const props = defineProps<{
note: entities.Note;
pinned?: boolean;
}>();
const rootEl = ref<HTMLElement | null>(null);
const note = ref(deepClone(props.note));
const deleted = computed(() => isDeleted(note.value.id));
let capture = useNoteCapture({
note,
rootEl,
});
function reload() {
note.value = deepClone(props.note);
capture.close();
capture = useNoteCapture({
note,
rootEl,
});
}
watch(
() => props.note.id,
(o, n) => {
if (o === n) return;
reload();
},
);
</script>
<style lang="scss" scoped>

View File

@ -3,6 +3,7 @@
ref="pagingComponent"
:pagination="pagination"
:disable-auto-load="disableAutoLoad"
:folder
>
<template #empty>
<div class="_fullinfo">
@ -15,7 +16,7 @@
</div>
</template>
<template #default="{ items: notes }">
<template #default="{ foldedItems: notes }">
<div ref="tlEl" class="giivymft" :class="{ noGap }">
<XList
ref="notes"
@ -28,6 +29,21 @@
class="notes"
>
<XNote
v-if="'folded' in note && note.folded === 'thread'"
:key="note.id"
class="qtqtichx"
:note="note.note"
:parents="note.parents"
/>
<XNote
v-else-if="'folded' in note && note.folded === 'renote'"
:key="note.key"
class="qtqtichx"
:note="note.note"
:renotes="note.renotesArr"
/>
<XNote
v-else
:key="note._featuredId_ || note._prId_ || note.id"
class="qtqtichx"
:note="note"
@ -51,14 +67,21 @@ import XList from "@/components/MkDateSeparatedList.vue";
import MkPagination from "@/components/MkPagination.vue";
import { i18n } from "@/i18n";
import { scroll } from "@/scripts/scroll";
import type { NoteFolded, NoteThread, NoteType } from "@/types/note";
const tlEl = ref<HTMLElement>();
defineProps<{
pagination: PagingOf<entities.Note>;
noGap?: boolean;
disableAutoLoad?: boolean;
}>();
withDefaults(
defineProps<{
pagination: PagingOf<entities.Note>;
noGap?: boolean;
disableAutoLoad?: boolean;
folder?: (ns: entities.Note[]) => (NoteType | NoteThread | NoteFolded)[];
}>(),
{
folder: (ns: entities.Note[]) => ns,
},
);
const pagingComponent = ref<MkPaginationType<
PagingKeyOf<entities.Note>

View File

@ -79,29 +79,35 @@ const stream = useStream();
const pagingComponent = ref<MkPaginationType<"i/notifications"> | null>(null);
const shouldFold = defaultStore.state.foldNotification;
const shouldFold = defaultStore.reactiveState.foldNotification;
const convertNotification = computed(() =>
shouldFold.value ? foldNotifications : (ns: entities.Notification[]) => ns,
);
const FETCH_LIMIT = 90;
const pagination = Object.assign(
{
endpoint: "i/notifications" as const,
params: computed(() => ({
includeTypes: props.includeTypes ?? undefined,
excludeTypes: props.includeTypes
? undefined
: me?.mutingNotificationTypes,
unreadOnly: props.unreadOnly,
})),
},
shouldFold
? {
limit: 50,
secondFetchLimit: FETCH_LIMIT,
}
: {
limit: 30,
},
const pagination = computed(() =>
Object.assign(
{
endpoint: "i/notifications" as const,
params: computed(() => ({
includeTypes: props.includeTypes ?? undefined,
excludeTypes: props.includeTypes
? undefined
: me?.mutingNotificationTypes,
unreadOnly: props.unreadOnly,
})),
},
shouldFold.value
? {
limit: 50,
secondFetchLimit: FETCH_LIMIT,
}
: {
limit: 30,
},
),
);
function isNoteNotification(
@ -138,14 +144,6 @@ const onNotification = (notification: entities.Notification) => {
let connection: StreamTypes.ChannelOf<"main"> | undefined;
function convertNotification(ns: entities.Notification[]) {
if (shouldFold) {
return foldNotifications(ns);
} else {
return ns;
}
}
onMounted(() => {
connection = stream.useChannel("main");
connection.on("notification", onNotification);

View File

@ -365,9 +365,9 @@ async function fetch(firstFetching?: boolean) {
}
// biome-ignore lint/style/noParameterAssign: assign it intentially
res = res.filter((item) => {
if (idMap.has(item)) return false;
idMap.set(item, true);
res = res.filter((it) => {
if (idMap.has(it.id)) return false;
idMap.set(it.id, true);
return true;
});
}
@ -435,8 +435,20 @@ const prepend = (...item: Item[]): void => {
}
};
const append = (...items: Item[]): void => {
appended.value.push(...items);
const append = (...it: Item[]): void => {
// If there are too many appended, merge them into arrItems
if (
appended.value.length >
(props.pagination.secondFetchLimit || SECOND_FETCH_LIMIT_DEFAULT)
) {
for (const item of appended.value) {
idMap.set(item.id, true);
}
arrItems.value.push(appended.value);
appended.value = [];
// We don't need to calculate here because it won't cause any changes in items
}
appended.value.push(...it);
calculateItems();
};
@ -486,6 +498,8 @@ if (props.pagination.params && isRef<Param>(props.pagination.params)) {
watch(props.pagination.params, reload, { deep: true });
}
watch(() => props.folder, calculateItems);
watch(
queue,
(a, b) => {

View File

@ -178,7 +178,7 @@
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { computed, ref, watch } from "vue";
import type { entities } from "firefish-js";
import * as mfm from "mfm-js";
import * as os from "@/os";
@ -226,24 +226,35 @@ const emit = defineEmits<{
const cwButton = ref<HTMLElement>();
const showMoreButton = ref<HTMLElement>();
const isLong =
!props.detailedView &&
props.note.cw == null &&
props.isLongJudger(props.note);
const collapsed = ref(props.note.cw == null && isLong);
const urls = props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
: null;
const showContent = ref(false);
const mfms = props.note.text
? extractMfmWithAnimation(mfm.parse(props.note.text))
: null;
const hasMfm = ref(mfms && mfms.length > 0);
const isLong = computed(
() =>
!props.detailedView &&
props.note.cw == null &&
props.isLongJudger(props.note),
);
const urls = computed(() =>
props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
: null,
);
const mfms = computed(() =>
props.note.text ? extractMfmWithAnimation(mfm.parse(props.note.text)) : null,
);
const hasMfm = computed(() => mfms.value && mfms.value.length > 0);
const disableMfm = ref(defaultStore.state.animatedMfm);
const showContent = ref(false);
const collapsed = ref(props.note.cw == null && isLong.value);
watch(
() => props.note.id,
(o, n) => {
if (o !== n) return;
disableMfm.value = defaultStore.state.animatedMfm;
showContent.value = false;
collapsed.value = props.note.cw == null && isLong.value;
},
);
async function toggleMfm() {
if (disableMfm.value) {

View File

@ -30,6 +30,7 @@
:pagination="pagination"
@queue="(x) => (queue = x)"
@status="pullToRefreshComponent?.setDisabled($event)"
:folder
/>
</MkPullToRefresh>
<XNotes
@ -39,6 +40,7 @@
:pagination="pagination"
@queue="(x) => (queue = x)"
@status="pullToRefreshComponent?.setDisabled($event)"
:folder
/>
</template>
@ -54,6 +56,8 @@ import { isSignedIn, me } from "@/me";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
import { foldNotes } from "@/scripts/fold";
import type { NoteType } from "@/types/note";
export type TimelineSource =
| "antenna"
@ -85,6 +89,12 @@ const emit = defineEmits<{
const tlComponent = ref<InstanceType<typeof XNotes>>();
const pullToRefreshComponent = ref<InstanceType<typeof MkPullToRefresh>>();
const folder = computed(() => {
const mergeThread = defaultStore.reactiveState.mergeThreadInTimeline.value;
const mergeRenotes = defaultStore.reactiveState.mergeRenotesInTimeline.value;
return (ns: NoteType[]) => foldNotes(ns, mergeThread, mergeRenotes);
});
let endpoint: TypeUtils.EndpointsOf<entities.Note[]>; // keyof Endpoints
let query: {
antennaId?: string | undefined;

View File

@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from "vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { i18n } from "@/i18n";
import { dateTimeFormat } from "@/scripts/intl-const";
@ -25,7 +25,7 @@ const props = withDefaults(
},
);
const _time =
const _time = computed(() =>
props.time == null
? Number.NaN
: typeof props.time === "number"
@ -33,16 +33,19 @@ const _time =
: (props.time instanceof Date
? props.time
: new Date(props.time)
).getTime();
const invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
).getTime(),
);
const invalid = computed(() => Number.isNaN(_time.value));
const absolute = computed(() =>
!invalid.value ? dateTimeFormat.format(_time.value) : i18n.ts._ago.invalid,
);
const now = ref(props.origin?.getTime() ?? Date.now());
const relative = computed<string>(() => {
if (props.mode === "absolute") return ""; // absoluterelative使
if (invalid) return i18n.ts._ago.invalid;
if (invalid.value) return i18n.ts._ago.invalid;
const ago = (now.value - _time) / 1000; /* ms */
const ago = (now.value - _time.value) / 1000; /* ms */
return ago >= 31536000
? i18n.t("_ago.yearsAgo", { n: Math.floor(ago / 31536000).toString() })
: ago >= 2592000
@ -74,15 +77,25 @@ const relative = computed<string>(() => {
: i18n.ts._ago.future;
});
let tickId: number;
let tickId: number | undefined;
function tick() {
if (
invalid.value ||
props.origin ||
(props.mode !== "relative" && props.mode !== "detail")
) {
if (tickId) window.clearInterval(tickId);
tickId = undefined;
return;
}
const _now = Date.now();
const agoPrev = (now.value - _time) / 1000; /* ms */ // interval
const agoPrev = (now.value - _time.value) / 1000; /* ms */ // interval
now.value = _now;
const ago = (now.value - _time) / 1000; /* ms */ // interval
const ago = (now.value - _time.value) / 1000; /* ms */ // interval
const prev = agoPrev < 60 ? 10000 : agoPrev < 3600 ? 60000 : 180000;
const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
@ -94,16 +107,13 @@ function tick() {
}
}
if (
!invalid &&
!props.origin &&
(props.mode === "relative" || props.mode === "detail")
) {
onMounted(() => {
tick();
});
onUnmounted(() => {
if (tickId) window.clearInterval(tickId);
});
}
watch(() => props.time, tick);
onMounted(() => {
tick();
});
onUnmounted(() => {
if (tickId) window.clearInterval(tickId);
});
</script>

View File

@ -140,6 +140,12 @@
<FormSwitch v-model="foldNotification" class="_formBlock">{{
i18n.ts.foldNotification
}}</FormSwitch>
<FormSwitch v-model="mergeThreadInTimeline" class="_formBlock">{{
i18n.ts.mergeThreadInTimeline
}}</FormSwitch>
<FormSwitch v-model="mergeRenotesInTimeline" class="_formBlock">{{
i18n.ts.mergeRenotesInTimeline
}}</FormSwitch>
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@ -556,6 +562,12 @@ const autocorrectNoteLanguage = computed(
const foldNotification = computed(
defaultStore.makeGetterSetter("foldNotification"),
);
const mergeThreadInTimeline = computed(
defaultStore.makeGetterSetter("mergeThreadInTimeline"),
);
const mergeRenotesInTimeline = computed(
defaultStore.makeGetterSetter("mergeRenotesInTimeline"),
);
// This feature (along with injectPromo) is currently disabled
// function onChangeInjectFeaturedNote(v) {
@ -632,7 +644,6 @@ watch(
enableTimelineStreaming,
enablePullToRefresh,
pullToRefreshThreshold,
foldNotification,
],
async () => {
await reloadAsk();

View File

@ -3,6 +3,9 @@ import type {
FoldableNotification,
NotificationFolded,
} from "@/types/notification";
import type { NoteType, NoteThread, NoteFolded } from "@/types/note";
import { me } from "@/me";
import { isDeleted, isRenote } from "./note";
interface FoldOption {
/** If items length is 1, skip aggregation */
@ -91,3 +94,94 @@ export function foldNotifications(ns: entities.Notification[]) {
},
);
}
export function foldNotes(ns: NoteType[], foldReply = true, foldRenote = true) {
// By the implement of MkPagination, lastId is unique and is safe for key
const lastId = ns[ns.length - 1]?.id ?? "prepend";
function foldReplies(ns: NoteType[]) {
const res: Array<NoteType | NoteThread> = [];
const threads = new Map<NoteType["id"], NoteType[]>();
for (const n of [...ns].reverse()) {
if (isDeleted(n.id)) {
continue;
}
if (n.replyId && threads.has(n.replyId)) {
const th = threads.get(n.replyId)!;
threads.delete(n.replyId);
th.push(n);
threads.set(n.id, th);
} else if (n.reply?.replyId && threads.has(n.reply.replyId)) {
const th = threads.get(n.reply.replyId)!;
threads.delete(n.reply.replyId);
th.push(n.reply, n);
threads.set(n.id, th);
} else {
threads.set(n.id, [n]);
}
}
for (const n of ns) {
const conversation = threads.get(n.id);
if (conversation == null) continue;
const first = conversation[0];
const last = conversation[conversation.length - 1];
if (conversation.length === 1) {
res.push(first);
continue;
}
res.push({
// The same note can only appear once in the timeline, so the ID will not be repeated
id: first.id,
createdAt: last.createdAt,
folded: "thread",
note: last,
parents: (first.reply ? [first.reply] : []).concat(
conversation.slice(0, -1),
),
});
}
return res;
}
let res: (NoteType | NoteThread | NoteFolded)[] = ns;
if (foldReply) {
res = foldReplies(ns);
}
if (foldRenote) {
res = foldItems(
res,
(n) => {
// never fold my renotes
if (!("folded" in n) && isRenote(n) && n.userId !== me?.id)
return `renote-${n.renoteId}`;
return n.id;
},
(ns, key) => {
const represent = ns[0];
if (!key.startsWith("renote-")) {
return represent;
}
return {
id: `G-${lastId}-${key}`,
key: `G-${lastId}-${key}`,
createdAt: represent.createdAt,
folded: "renote",
note: (represent as entities.Note).renote!,
renotesArr: ns as entities.Note[],
};
},
{
skipSingleElement: false,
},
);
}
return res;
}

View File

@ -0,0 +1,20 @@
import type { entities } from "firefish-js";
import { deletedNoteIds } from "./use-note-capture";
export function isRenote(note: entities.Note): note is entities.Note & {
renote: entities.Note;
text: null;
renoteId: string;
poll: undefined;
} {
return (
note.renote != null &&
note.text == null &&
note.fileIds.length === 0 &&
note.poll == null
);
}
export function isDeleted(noteId: string) {
return deletedNoteIds.has(noteId);
}

View File

@ -1,22 +1,62 @@
import type { Ref } from "vue";
import { onUnmounted } from "vue";
import type { entities } from "firefish-js";
import { onUnmounted, ref } from "vue";
import { useStream } from "@/stream";
import { isSignedIn, me } from "@/me";
import * as os from "@/os";
import type { NoteType } from "@/types/note";
export const deletedNoteIds = new Map<NoteType["id"], boolean>();
const noteRefs = new Map<NoteType["id"], Ref<NoteType>[]>();
function addToNoteRefs(note: Ref<NoteType>) {
const refs = noteRefs.get(note.value.id);
if (refs) {
refs.push(note);
} else {
noteRefs.set(note.value.id, [note]);
}
}
function eachNote(id: NoteType["id"], cb: (note: Ref<NoteType>) => void) {
const refs = noteRefs.get(id);
if (refs) {
for (const n of refs) {
// n.value.id maybe changed
if (n.value.id === id) {
cb(n);
}
}
}
}
export function useNoteCapture(props: {
rootEl: Ref<HTMLElement | null>;
note: Ref<entities.Note>;
isDeletedRef: Ref<boolean>;
onReplied?: (note: entities.Note) => void;
note: Ref<NoteType>;
isDeletedRef?: Ref<boolean>;
onReplied?: (note: NoteType) => void;
}) {
let closed = false;
const note = props.note;
const connection = isSignedIn(me) ? useStream() : null;
addToNoteRefs(note);
function onDeleted() {
if (props.isDeletedRef) props.isDeletedRef.value = true;
deletedNoteIds.set(note.value.id, true);
if (note.value.replyId) {
eachNote(note.value.replyId, (n) => n.value.repliesCount--);
}
if (note.value.renoteId) {
eachNote(note.value.renoteId, (n) => n.value.renoteCount--);
}
}
async function onStreamNoteUpdated(noteData): Promise<void> {
const { type, id, body } = noteData;
if (closed) return;
if (id !== note.value.id) return;
switch (type) {
@ -87,7 +127,7 @@ export function useNoteCapture(props: {
}
case "deleted": {
props.isDeletedRef.value = true;
onDeleted();
break;
}
@ -96,17 +136,14 @@ export function useNoteCapture(props: {
const editedNote = await os.api("notes/show", {
noteId: id,
});
const keys = new Set<string>();
Object.keys(editedNote)
.concat(Object.keys(note.value))
.forEach((key) => keys.add(key));
keys.forEach((key) => {
for (const key of [
...new Set(Object.keys(editedNote).concat(Object.keys(note.value))),
]) {
note.value[key] = editedNote[key];
});
}
} catch {
// delete the note if failing to get the edited note
props.isDeletedRef.value = true;
onDeleted();
}
break;
}
@ -147,4 +184,10 @@ export function useNoteCapture(props: {
connection.off("_connected_", onStreamConnected);
}
});
return {
close: () => {
closed = true;
},
};
}

View File

@ -454,6 +454,14 @@ export const defaultStore = markRaw(
where: "deviceAccount",
default: true,
},
mergeThreadInTimeline: {
where: "deviceAccount",
default: true,
},
mergeRenotesInTimeline: {
where: "deviceAccount",
default: true,
},
}),
);

View File

@ -1,4 +1,4 @@
import type { noteVisibilities } from "firefish-js";
import type { entities, noteVisibilities } from "firefish-js";
export type NoteVisibility = (typeof noteVisibilities)[number] | "private";
@ -6,3 +6,25 @@ export interface NoteTranslation {
sourceLang: string;
text: string;
}
export type NoteType = entities.Note & {
_featuredId_?: string;
_prId_?: string;
};
export type NoteFolded = {
id: string;
key: string;
createdAt: entities.Note["createdAt"];
folded: "renote";
note: entities.Note;
renotesArr: entities.Note[];
};
export type NoteThread = {
id: string;
createdAt: entities.Note["createdAt"];
folded: "thread";
note: entities.Note;
parents: entities.Note[];
};

View File

@ -153,9 +153,6 @@ importers:
gunzip-maybe:
specifier: ^1.4.2
version: 1.4.2
happy-dom:
specifier: ^14.7.1
version: 14.7.1
hpagent:
specifier: 1.2.0
version: 1.2.0
@ -168,6 +165,9 @@ importers:
is-svg:
specifier: 5.0.0
version: 5.0.0
jsdom:
specifier: 24.0.0
version: 24.0.0
json5:
specifier: 2.2.3
version: 2.2.3
@ -333,9 +333,6 @@ importers:
uuid:
specifier: 9.0.1
version: 9.0.1
web-push:
specifier: 3.6.7
version: 3.6.7
websocket:
specifier: 1.0.34
version: 1.0.34
@ -368,6 +365,9 @@ importers:
'@types/fluent-ffmpeg':
specifier: 2.1.24
version: 2.1.24
'@types/jsdom':
specifier: 21.1.6
version: 21.1.6
'@types/jsonld':
specifier: 1.5.13
version: 1.5.13
@ -4114,6 +4114,14 @@ packages:
pretty-format: 29.7.0
dev: true
/@types/jsdom@21.1.6:
resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==}
dependencies:
'@types/node': 20.12.7
'@types/tough-cookie': 4.0.5
parse5: 7.1.2
dev: true
/@types/json-schema@7.0.12:
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
dev: true
@ -4416,6 +4424,10 @@ packages:
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
dev: true
/@types/tough-cookie@4.0.5:
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
dev: true
/@types/unist@2.0.7:
resolution: {integrity: sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==}
dev: true
@ -5174,8 +5186,8 @@ packages:
- supports-color
dev: false
/agent-base@7.1.0:
resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
/agent-base@7.1.1:
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
engines: {node: '>= 14'}
dependencies:
debug: 4.3.4(supports-color@8.1.1)
@ -5439,15 +5451,6 @@ packages:
/asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
/asn1.js@5.4.1:
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
dependencies:
bn.js: 4.12.0
inherits: 2.0.4
minimalistic-assert: 1.0.1
safer-buffer: 2.1.2
dev: false
/asn1@0.2.6:
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
dependencies:
@ -5700,10 +5703,6 @@ packages:
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
dev: false
/bn.js@4.12.0:
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
dev: false
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@ -5798,10 +5797,6 @@ packages:
engines: {node: '>=8.0.0'}
dev: false
/buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
dev: false
/buffer-fill@1.0.0:
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
dev: false
@ -6712,6 +6707,13 @@ packages:
hasBin: true
dev: true
/cssstyle@4.0.1:
resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==}
engines: {node: '>=18'}
dependencies:
rrweb-cssom: 0.6.0
dev: false
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
dev: true
@ -6743,6 +6745,14 @@ packages:
engines: {node: '>= 12'}
dev: false
/data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.0.0
dev: false
/date-fns@3.6.0:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
@ -6816,6 +6826,10 @@ packages:
engines: {node: '>=10'}
dev: true
/decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
dev: false
/decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
@ -7119,12 +7133,6 @@ packages:
safer-buffer: 2.1.2
dev: false
/ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
safe-buffer: 5.2.1
dev: false
/editorconfig@1.0.4:
resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
engines: {node: '>=14'}
@ -9019,15 +9027,6 @@ packages:
engines: {node: '>=0.8.0'}
dev: true
/happy-dom@14.7.1:
resolution: {integrity: sha512-v60Q0evZ4clvMcrAh5/F8EdxDdfHdFrtffz/CNe10jKD+nFweZVxM91tW+UyY2L4AtpgIaXdZ7TQmiO1pfcwbg==}
engines: {node: '>=16.0.0'}
dependencies:
entities: 4.5.0
webidl-conversions: 7.0.0
whatwg-mimetype: 3.0.0
dev: false
/hard-rejection@2.1.0:
resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
engines: {node: '>=6'}
@ -9129,6 +9128,13 @@ packages:
engines: {node: '>=14'}
dev: false
/html-encoding-sniffer@4.0.0:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
dependencies:
whatwg-encoding: 3.1.1
dev: false
/html-entities@2.3.2:
resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==}
dev: false
@ -9199,6 +9205,16 @@ packages:
toidentifier: 1.0.1
dev: false
/http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.1
debug: 4.3.4(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
dev: false
/http2-wrapper@1.0.3:
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
engines: {node: '>=10.19.0'}
@ -9214,11 +9230,6 @@ packages:
resolve-alpn: 1.2.1
dev: false
/http_ece@1.2.0:
resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==}
engines: {node: '>=16'}
dev: false
/https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@ -9229,11 +9240,11 @@ packages:
- supports-color
dev: false
/https-proxy-agent@7.0.1:
resolution: {integrity: sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ==}
/https-proxy-agent@7.0.4:
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.0
agent-base: 7.1.1
debug: 4.3.4(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -9606,6 +9617,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
dev: false
/is-promise@2.2.2:
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
@ -10375,6 +10390,42 @@ packages:
engines: {node: '>=12.0.0'}
dev: true
/jsdom@24.0.0:
resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==}
engines: {node: '>=18'}
peerDependencies:
canvas: ^2.11.2
peerDependenciesMeta:
canvas:
optional: true
dependencies:
cssstyle: 4.0.1
data-urls: 5.0.0
decimal.js: 10.4.3
form-data: 4.0.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.4
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.9
parse5: 7.1.2
rrweb-cssom: 0.6.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 4.1.4
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.0.0
ws: 8.16.0
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
dev: false
/jsesc@0.5.0:
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
hasBin: true
@ -10512,21 +10563,6 @@ packages:
is-promise: 2.2.2
promise: 7.3.1
/jwa@2.0.0:
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
dev: false
/jws@4.0.0:
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
dependencies:
jwa: 2.0.0
safe-buffer: 5.2.1
dev: false
/katex@0.16.10:
resolution: {integrity: sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==}
hasBin: true
@ -11185,10 +11221,6 @@ packages:
engines: {node: '>=4'}
dev: true
/minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
dev: false
/minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
@ -11579,6 +11611,10 @@ packages:
boolbase: 1.0.0
dev: true
/nwsapi@2.2.9:
resolution: {integrity: sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==}
dev: false
/oauth@0.10.0:
resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==}
dev: false
@ -11885,7 +11921,6 @@ packages:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
dependencies:
entities: 4.5.0
dev: false
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
@ -12365,6 +12400,10 @@ packages:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
dev: true
/psl@1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
dev: false
/pug-attrs@3.0.0:
resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==}
dependencies:
@ -12524,6 +12563,10 @@ packages:
deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
dev: false
/querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: false
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
@ -12760,6 +12803,10 @@ packages:
/require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@ -12886,6 +12933,10 @@ packages:
fsevents: 2.3.3
dev: true
/rrweb-cssom@0.6.0:
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
dev: false
/rss-parser@3.13.0:
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
dependencies:
@ -12964,6 +13015,13 @@ packages:
/sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
/saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
dependencies:
xmlchars: 2.2.0
dev: false
/schema-utils@3.3.0:
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
engines: {node: '>= 10.13.0'}
@ -13569,6 +13627,10 @@ packages:
engines: {node: '>= 4.7.0'}
dev: true
/symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
dev: false
/synckit@0.6.2:
resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==}
engines: {node: '>=12.20'}
@ -13788,9 +13850,26 @@ packages:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
/tough-cookie@4.1.4:
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
engines: {node: '>=6'}
dependencies:
psl: 1.9.0
punycode: 2.3.1
universalify: 0.2.0
url-parse: 1.5.10
dev: false
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
/tr46@5.0.0:
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
engines: {node: '>=18'}
dependencies:
punycode: 2.3.1
dev: false
/trace-redirect@1.0.6:
resolution: {integrity: sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==}
dev: false
@ -14270,6 +14349,11 @@ packages:
engines: {node: '>= 4.0.0'}
dev: false
/universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
dev: false
/universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
@ -14310,6 +14394,13 @@ packages:
dependencies:
punycode: 2.3.1
/url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
dev: false
/url-polyfill@1.1.12:
resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==}
dev: true
@ -14581,6 +14672,13 @@ packages:
typescript: 5.4.5
dev: true
/w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
dependencies:
xml-name-validator: 5.0.0
dev: false
/walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
dependencies:
@ -14605,20 +14703,6 @@ packages:
defaults: 1.0.4
dev: true
/web-push@3.6.7:
resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==}
engines: {node: '>= 16'}
hasBin: true
dependencies:
asn1.js: 5.4.1
http_ece: 1.2.0
https-proxy-agent: 7.0.1
jws: 4.0.0
minimist: 1.2.8
transitivePeerDependencies:
- supports-color
dev: false
/web-streams-polyfill@3.2.1:
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
engines: {node: '>= 8'}
@ -14691,9 +14775,24 @@ packages:
- supports-color
dev: false
/whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
/whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
dependencies:
iconv-lite: 0.6.3
dev: false
/whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
dev: false
/whatwg-url@14.0.0:
resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==}
engines: {node: '>=18'}
dependencies:
tr46: 5.0.0
webidl-conversions: 7.0.0
dev: false
/whatwg-url@5.0.0:
@ -14823,7 +14922,6 @@ packages:
optional: true
utf-8-validate:
optional: true
dev: true
/xev@3.0.2:
resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==}
@ -14841,6 +14939,11 @@ packages:
engines: {node: '>=12'}
dev: true
/xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
dev: false
/xml2js@0.5.0:
resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
engines: {node: '>=4.0.0'}
@ -14862,6 +14965,10 @@ packages:
engines: {node: '>=4.0'}
dev: false
/xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}