Compare commits
46 commits
3f68e977c6
...
1aec738200
Author | SHA1 | Date | |
---|---|---|---|
1aec738200 | |||
be726a45f9 | |||
bd00d121cb | |||
daea4247b8 | |||
|
85d5e7bd3b | ||
1ccf256b99 | |||
acc70d3d80 | |||
54ce89ff9f | |||
41b25228df | |||
81ab8de2fc | |||
80448cd3ac | |||
6699f6a34f | |||
68285f7165 | |||
468073cba8 | |||
55514bace4 | |||
e3fc84b80f | |||
0e60607e27 | |||
7ce992956a | |||
01a1a58fa3 | |||
d12b7cb941 | |||
8a46b92948 | |||
5ce9d75661 | |||
17598c2248 | |||
17284bbab4 | |||
d006fe9190 | |||
5307338de9 | |||
657a242b90 | |||
d7d9e3c323 | |||
|
8d044c617d | ||
21aa6b097b | |||
|
89d058ddc8 | ||
3a0a2d04f3 | |||
27aeca9c2d | |||
23ab5567dd | |||
475e2877f1 | |||
e81cc3bfdd | |||
16b4f78dfb | |||
7f056e729f | |||
11541b1e63 | |||
|
a09b8e4d80 | ||
7c887421f4 | |||
|
03807b7815 | ||
|
e8fc5ec37e | ||
f0d82ebb94 | |||
1d5372282f | |||
|
a53e0d18cf |
178 changed files with 2219 additions and 1736 deletions
4
.config/docker.example.env
Normal file
4
.config/docker.example.env
Normal file
|
@ -0,0 +1,4 @@
|
|||
# db settings
|
||||
POSTGRES_PASSWORD=very_strong_password
|
||||
POSTGRES_USER=firefish
|
||||
POSTGRES_DB=firefish_db
|
|
@ -1,4 +0,0 @@
|
|||
# db settings
|
||||
POSTGRES_PASSWORD=example-firefish-pass
|
||||
POSTGRES_USER=example-firefish-user
|
||||
POSTGRES_DB=firefish
|
|
@ -31,21 +31,26 @@ port: 3000
|
|||
|
||||
# The bind host your Firefish server should listen on.
|
||||
# If unspecified, the wildcard address will be used.
|
||||
#bind: 127.0.0.1
|
||||
# You may need to comment out the following line if you use Docker/Podman.
|
||||
bind: 127.0.0.1
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
db:
|
||||
# If you use docker-compose or podman compose with the default settings,
|
||||
# you need to change this to firefish_db
|
||||
host: localhost
|
||||
|
||||
port: 5432
|
||||
#ssl: false
|
||||
|
||||
# Database name
|
||||
db: firefish
|
||||
db: firefish_db
|
||||
|
||||
# Auth
|
||||
user: example-firefish-user
|
||||
pass: example-firefish-pass
|
||||
user: firefish
|
||||
pass: very_strong_password
|
||||
|
||||
# Whether disable Caching queries
|
||||
#disableCache: true
|
||||
|
@ -60,7 +65,10 @@ db:
|
|||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
||||
redis:
|
||||
# If you use docker-compose or podman compose with the default settings,
|
||||
# you need to change this to firefish_redis
|
||||
host: localhost
|
||||
|
||||
port: 6379
|
||||
#tls:
|
||||
# host: localhost
|
||||
|
@ -105,10 +113,10 @@ redis:
|
|||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Maximum length of a post (default 3000, max 100000)
|
||||
#maxNoteLength: 3000
|
||||
maxNoteLength: 3000
|
||||
|
||||
# Maximum length of an image caption (default 1500, max 8192)
|
||||
#maxCaptionLength: 1500
|
||||
maxCaptionLength: 1500
|
||||
|
||||
# Reserved usernames that only the administrator can register with
|
||||
reservedUsernames: [
|
||||
|
@ -133,8 +141,8 @@ reservedUsernames: [
|
|||
# inboxJobConcurrency: 16
|
||||
|
||||
# Job rate limiter
|
||||
# deliverJobPerSec: 128
|
||||
# inboxJobPerSec: 16
|
||||
deliverJobPerSec: 128
|
||||
inboxJobPerSec: 128
|
||||
|
||||
# Job attempts
|
||||
# deliverJobMaxAttempts: 12
|
||||
|
@ -169,7 +177,7 @@ reservedUsernames: [
|
|||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: false)
|
||||
#proxyRemoteFiles: true
|
||||
proxyRemoteFiles: true
|
||||
|
||||
# Use authorized fetch for outgoing requests
|
||||
signToActivityPubGet: true
|
||||
|
|
|
@ -42,3 +42,6 @@ packages/backend/assets/instance.css
|
|||
.git
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
|
||||
# Auto-generated files
|
||||
/neko/volume
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -24,7 +24,7 @@ coverage
|
|||
/.config/*
|
||||
!/.config/example.yml
|
||||
!/.config/devenv.yml
|
||||
!/.config/docker_example.env
|
||||
!/.config/docker.example.env
|
||||
!/.config/docker_ci.env
|
||||
!/.config/helm_values_example.yml
|
||||
!/.config/LICENSE
|
||||
|
@ -79,3 +79,6 @@ yarn*
|
|||
# Cargo cache for Docker
|
||||
/.cargo-cache
|
||||
/.cargo-target
|
||||
|
||||
# Custom
|
||||
custom/
|
||||
|
|
33
Dockerfile
33
Dockerfile
|
@ -1,15 +1,18 @@
|
|||
## Install dev and compilation dependencies, build files
|
||||
FROM node:21-slim as build
|
||||
FROM docker.io/node:21-slim as build
|
||||
WORKDIR /firefish
|
||||
|
||||
# Install compilation dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y python3 git wget curl build-essential
|
||||
RUN mkdir -m777 /opt/rust /opt/cargo
|
||||
ENV RUSTUP_HOME=/opt/rust CARGO_HOME=/opt/cargo PATH=/opt/cargo/bin:$PATH
|
||||
RUN wget --https-only --secure-protocol=TLSv1_2 -O- https://sh.rustup.rs | sh /dev/stdin -y
|
||||
RUN printf '#!/bin/sh\nexport CARGO_HOME=/opt/cargo\nexec /bin/sh "$@"\n' >/usr/local/bin/sh
|
||||
RUN chmod +x /usr/local/bin/sh
|
||||
RUN apt-get update && DEBIAN_FRONTEND='noninteractive' apt-get install -y --no-install-recommends curl build-essential ca-certificates clang
|
||||
RUN curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN echo 'deb https://deb.debian.org/debian testing main' | tee /etc/apt/sources.list
|
||||
RUN apt-get update && DEBIAN_FRONTEND='noninteractive' apt-get --target-release testing install -y --no-install-recommends mold
|
||||
RUN <<EOC
|
||||
echo "[target.x86_64-unknown-linux-gnu]\nlinker = '$(which clang)'\nrustflags = ['-C', 'link-arg=--ld-path=$(which mold)']" > /root/.cargo/config
|
||||
echo "[target.aarch64-unknown-linux-gnu]\nlinker = '$(which clang)'\nrustflags = ['-C', 'link-arg=--ld-path=$(which mold)']" >> /root/.cargo/config
|
||||
EOC
|
||||
|
||||
# Copy only the cargo dependency-related files first, to cache efficiently
|
||||
COPY packages/backend/native-utils/Cargo.toml packages/backend/native-utils/Cargo.toml
|
||||
|
@ -56,15 +59,14 @@ RUN env NODE_ENV=production sh -c "pnpm run --filter '!native-utils' build && pn
|
|||
RUN pnpm install --prod --frozen-lockfile
|
||||
|
||||
## Runtime container
|
||||
FROM node:21-slim
|
||||
FROM docker.io/node:21-slim
|
||||
WORKDIR /firefish
|
||||
|
||||
# Install runtime dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends zip unzip tini ffmpeg ca-certificates
|
||||
RUN apt-get update && DEBIAN_FRONTEND='noninteractive' apt-get install -y --no-install-recommends zip unzip tini ffmpeg ca-certificates
|
||||
|
||||
RUN echo 'deb https://deb.debian.org/debian experimental main' | tee /etc/apt/sources.list
|
||||
RUN apt-get update && apt-get --target-release experimental install -y --no-install-recommends libc6
|
||||
RUN apt-get update && DEBIAN_FRONTEND='noninteractive' apt-get --target-release experimental install -y --no-install-recommends libc6
|
||||
|
||||
COPY . ./
|
||||
|
||||
|
@ -84,7 +86,12 @@ COPY --from=build /firefish/packages/backend/assets/instance.css /firefish/packa
|
|||
COPY --from=build /firefish/packages/backend/native-utils/built /firefish/packages/backend/native-utils/built
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
ARG VERSION
|
||||
ENV VERSION=${VERSION}
|
||||
RUN pnpm pkg set version="${VERSION}"
|
||||
|
||||
ENV NODE_ENV=production
|
||||
VOLUME "/firefish/files"
|
||||
ENTRYPOINT [ "/usr/bin/tini", "--" ]
|
||||
CMD [ "pnpm", "run", "migrateandstart" ]
|
||||
|
||||
CMD [ "pnpm", "run", "start:container" ]
|
||||
|
|
15
Makefile
Normal file
15
Makefile
Normal file
|
@ -0,0 +1,15 @@
|
|||
.PHONY: all
|
||||
all: build_image
|
||||
|
||||
|
||||
.PHONY: build_image
|
||||
build_image:
|
||||
. neko/update/utils && \
|
||||
buildah build \
|
||||
--no-cache \
|
||||
--platform linux/amd64 \
|
||||
--build-arg "VERSION=$$(version_ci)" \
|
||||
--tag docker.io/naskya/firefish \
|
||||
--tag registry.code.naskya.net/naskya/firefish \
|
||||
--tag "registry.code.naskya.net/naskya/firefish:$$(version_ci | cut -d':' -f2)" \
|
||||
.
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
## 主要な変更点
|
||||
|
||||
- 非ログインユーザーにもローカルタイムラインとグローバルタイムラインを公開できるように変更
|
||||
- コントロールパネルから設定すると `https://server.example.com/timeline` で公開されます
|
||||
- 検索フィルターを強化中
|
||||
- `from:me` を検索クエリの末尾につけると自分の投稿のみを検索できるように変更
|
||||
- 検索クエリの例: `予定 from:me`
|
||||
|
@ -38,6 +40,10 @@
|
|||
|
||||
## 細かい変更点
|
||||
|
||||
- 署名アルゴリズムとして ECDSA や Ed25519 なども受け入れる([github.com/mei23/misskey-v12](https://github.com/mei23/misskey-v12) から取り込み)
|
||||
- Pleroma のチャットに対応(Catodon から取り込み)
|
||||
- 翻訳機能にて、投稿言語が指定されていない場合にのみ言語の自動検出を用いるように変更
|
||||
- アップデート時に更新内容を確認できる機能を追加
|
||||
- 依存ライブラリを最新版にアップデート
|
||||
- ちゃんと動くか本家に push する前に実験したいという意図もあります
|
||||
- 中国語の猫モードでは 0.1 の確率で投稿の末尾に「喵」を追加するように
|
||||
|
@ -137,6 +143,7 @@
|
|||
|
||||
うまく動いていそうだったら本家に push されます
|
||||
|
||||
- Docker/Podman の環境で `custom` ディレクトリの内容が反映されない不具合を修正
|
||||
- 画面を下に引いてタイムラインなどを更新する機能を追加(Misskey から取り込み)
|
||||
- 本家にもマージリクエストを出しました ([!10644](https://git.joinfirefish.org/firefish/firefish/-/merge_requests/10644))
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ version: "3"
|
|||
|
||||
services:
|
||||
web:
|
||||
image: docker.io/naskya/firefish
|
||||
image: registry.code.naskya.net/naskya/firefish
|
||||
container_name: firefish_web
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
|
@ -16,9 +16,10 @@ services:
|
|||
environment:
|
||||
NODE_ENV: production
|
||||
volumes:
|
||||
- ./assets:/firefish/custom/assets
|
||||
- ./custom:/firefish/custom:ro
|
||||
- ./files:/firefish/files
|
||||
- ./.config:/firefish/.config:ro
|
||||
- ./neko/volume:/firefish/neko/volume:ro
|
||||
|
||||
redis:
|
||||
restart: unless-stopped
|
||||
|
|
|
@ -917,6 +917,7 @@ useBlurEffect: "Use blur effects in the UI"
|
|||
learnMore: "Learn more"
|
||||
misskeyUpdated: "Firefish has been updated!"
|
||||
whatIsNew: "Show changes"
|
||||
releaseNotes: "Changes"
|
||||
translate: "Translate"
|
||||
translatedFrom: "Translated from {x}"
|
||||
accountDeletionInProgress: "Account deletion is currently in progress"
|
||||
|
@ -2200,3 +2201,5 @@ moreUrls: "Pinned pages"
|
|||
moreUrlsDescription: "Enter the pages you want to pin to the help menu in the lower left corner using this notation:\n\"Display name\": https://example.com/"
|
||||
enablePullToRefresh: "Enable \"Pull down to refresh\""
|
||||
pullToRefreshThreshold: "Pull distance for reloading"
|
||||
publishTimelines: "Publish timelines for visitors"
|
||||
publishTimelinesDescription: "If enabled, the Local and Global timeline will be shown on {url} even when signed out."
|
||||
|
|
|
@ -831,6 +831,7 @@ useBlurEffect: "UIにぼかし効果を使用"
|
|||
learnMore: "詳しく"
|
||||
misskeyUpdated: "Firefishが更新されました!"
|
||||
whatIsNew: "更新情報を見る"
|
||||
releaseNotes: "更新情報"
|
||||
translate: "翻訳"
|
||||
translatedFrom: "{x}から翻訳"
|
||||
accountDeletionInProgress: "アカウントの削除が進行中です"
|
||||
|
@ -2042,3 +2043,5 @@ pullDownToReload: "下に引っ張って再読み込み"
|
|||
enableTimelineStreaming: "タイムラインを自動で更新する"
|
||||
enablePullToRefresh: "「下に引っ張って再読み込み」を有効にする"
|
||||
pullToRefreshThreshold: "再読み込みするために引っ張る距離"
|
||||
publishTimelines: "非ログインユーザーにもタイムラインを公開する"
|
||||
publishTimelinesDescription: "有効にすると、{url} でローカルタイムラインとグローバルタイムラインが公開されます。"
|
||||
|
|
|
@ -801,6 +801,7 @@ useBlurEffect: "在 UI 上使用模糊效果"
|
|||
learnMore: "更多信息"
|
||||
misskeyUpdated: "Firefish 更新完成!"
|
||||
whatIsNew: "显示更新信息"
|
||||
releaseNotes: "更新信息"
|
||||
translate: "翻译"
|
||||
translatedFrom: "从 {x} 翻译"
|
||||
accountDeletionInProgress: "正在删除账号"
|
||||
|
|
|
@ -797,6 +797,7 @@ useBlurEffect: "在 UI 上使用模糊效果"
|
|||
learnMore: "更多資訊"
|
||||
misskeyUpdated: "Firefish 更新完成!"
|
||||
whatIsNew: "顯示更新資訊"
|
||||
releaseNotes: "更新資訊"
|
||||
translate: "翻譯"
|
||||
translatedFrom: "從 {x} 翻譯"
|
||||
accountDeletionInProgress: "正在刪除帳戶"
|
||||
|
|
15
neko/messages/20240102_add_volume_to_docker_compose
Normal file
15
neko/messages/20240102_add_volume_to_docker_compose
Normal file
|
@ -0,0 +1,15 @@
|
|||
-------------------------------------------------------------------
|
||||
| For Docker users: |
|
||||
| Please add ./neko/volume:/firefish/neko/volume:ro to |
|
||||
| services.web.volumes in your docker-compose.yml before upgrading! |
|
||||
| |
|
||||
| After editing, services.web.volumes should look like this: |
|
||||
| volumes: |
|
||||
| - ./custom:/firefish/custom:ro |
|
||||
| - ./files:/firefish/files |
|
||||
| - ./.config:/firefish/.config:ro |
|
||||
| - ./neko/volume:/firefish/neko/volume:ro <-- add this line |
|
||||
| |
|
||||
| For the detailed explanation, see: |
|
||||
| https://post.naskya.net/notes/9nywqr2nkh0rjoum |
|
||||
-------------------------------------------------------------------
|
15
neko/messages/20240104_docker_registry_migration
Normal file
15
neko/messages/20240104_docker_registry_migration
Normal file
|
@ -0,0 +1,15 @@
|
|||
---------------------------------------------------
|
||||
| For Docker users: |
|
||||
| The Docker image will be migrated from docker.io |
|
||||
| to registry.code.naskya.net, so please edit your |
|
||||
| docker-compose.yml to use the following tag: |
|
||||
| |
|
||||
| registry.code.naskya.net/naskya/firefish:latest |
|
||||
| |
|
||||
| The image will be pushed to both registries until |
|
||||
| 2024-01-31, after which docker.io/naskya/firefish |
|
||||
| will be removed. |
|
||||
| |
|
||||
| For the detailed explanation, see: |
|
||||
| https://post.naskya.net/notes/9o27gac4dj8p483c |
|
||||
---------------------------------------------------
|
16
neko/messages/20240108_edit_docker_compose
Normal file
16
neko/messages/20240108_edit_docker_compose
Normal file
|
@ -0,0 +1,16 @@
|
|||
-----------------------------------------------------------
|
||||
| For Docker users: |
|
||||
| Sorry, there was an error in docker-compose.example.yml, |
|
||||
| so your docker-compose.yml is probably misconfigured too. |
|
||||
| |
|
||||
| Please replace |
|
||||
| |
|
||||
| - ./assets:/firefish/custom/assets |
|
||||
| |
|
||||
| in services.web.volumes with |
|
||||
| |
|
||||
| - ./custom:/firefish/custom:ro |
|
||||
| |
|
||||
| For the detailed explanation, see: |
|
||||
| https://post.naskya.net/notes/9o817o4hdezf7p8o |
|
||||
-----------------------------------------------------------
|
1882
neko/pnpm-lock.yaml
1882
neko/pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -5,16 +5,16 @@ set -eu
|
|||
|
||||
pull() {
|
||||
say 'Pulling the image...'
|
||||
run 'docker pull docker.io/naskya/firefish'
|
||||
run "$1 pull registry.code.naskya.net/naskya/firefish"
|
||||
}
|
||||
|
||||
if ! pull; then
|
||||
say 'awawa, the image may not be compatible with your environment...'
|
||||
if ! pull "$1"; then
|
||||
sadsay 'awawa, the image may not be compatible with your environment...'
|
||||
say 'Gonnya try building the image locally!'
|
||||
|
||||
say 'It takes some time! Why not brew a cup of cofe?'
|
||||
run "$(cat - << EOC
|
||||
docker build --tag docker.io/naskya/firefish --build-arg VERSION="$(version)" .
|
||||
$1 build --tag registry.code.naskya.net/naskya/firefish --build-arg VERSION="$(version)" .
|
||||
EOC
|
||||
)"
|
||||
fi
|
||||
|
|
|
@ -11,7 +11,7 @@ if [ "$#" != '1' ] || [ "$1" != '--skip-all-confirmations' ]; then
|
|||
|
||||
case "${yn}" in
|
||||
[Nn]|[Nn][Oo])
|
||||
say 'You must stop your server first!'
|
||||
sadsay 'You must stop your server first!'
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
|
@ -49,8 +49,9 @@ br
|
|||
|
||||
# prevent migration errors
|
||||
if [ ! -f packages/backend/native-utils/built/index.js ]; then
|
||||
say 'Something went wrong orz... Gonnya try fixing that.'
|
||||
sadsay 'Something went wrong... Gonnya try fixing that.'
|
||||
run 'cp neko/index.js packages/backend/native-utils/built/index.js'
|
||||
br
|
||||
else
|
||||
say "It's going well so far!"
|
||||
br
|
||||
|
|
7
neko/update/patch.sh
Executable file
7
neko/update/patch.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
[ ! -f neko/flags/install_pgroonga ] || mv neko/flags/install_pgroonga neko/flags/20231128_install_pgroonga
|
||||
[ ! -f neko/flags/docker_compose_rename ] || mv neko/flags/docker_compose_rename neko/flags/20231129_docker_compose_rename
|
||||
[ ! -f neko/flags/temp_upgrade_node_to_v21 ] || mv neko/flags/temp_upgrade_node_to_v21 neko/flags/20231229_upgrade_node_to_v21
|
||||
[ ! -f neko/flags/add_volume_to_docker_compose ] || mv neko/flags/add_volume_to_docker_compose neko/flags/20240102_add_volume_to_docker_compose
|
|
@ -10,6 +10,12 @@ say() {
|
|||
color
|
||||
}
|
||||
|
||||
sadsay() {
|
||||
color 5 # magenta
|
||||
printf '( T-T) < %s\n' "$1"
|
||||
color
|
||||
}
|
||||
|
||||
run() {
|
||||
color 3 # yellow
|
||||
printf '[running] $ %s\n' "$1"
|
||||
|
@ -24,13 +30,13 @@ br() {
|
|||
version() {
|
||||
UPSTREAM_VERSION=$(pnpm pkg get version | sed -e 's/"//g')
|
||||
COMMIT_DATE=$(git show --no-patch --pretty='%cs' FETCH_HEAD | sed -e 's/-//g' | cut -c 3-)
|
||||
COMMIT_HASH_INITIAL=$(printf '%s' "$(git rev-parse FETCH_HEAD)" | cut -c 1)
|
||||
COMMIT_HASH_INITIAL=$(printf '%s' "$(git rev-parse FETCH_HEAD)" | cut -c 1-2)
|
||||
printf '%s+neko:%s.%s' "${UPSTREAM_VERSION}" "${COMMIT_DATE}" "${COMMIT_HASH_INITIAL}"
|
||||
}
|
||||
|
||||
version_ci() {
|
||||
UPSTREAM_VERSION=$(grep '"version":' package.json | cut -d '"' -f 4)
|
||||
COMMIT_DATE=$(git show --no-patch --pretty='%cs' HEAD | sed -e 's/-//g' | cut -c 3-)
|
||||
COMMIT_HASH_INITIAL=$(printf '%s' "$(git rev-parse HEAD)" | cut -c 1)
|
||||
COMMIT_HASH_INITIAL=$(printf '%s' "$(git rev-parse HEAD)" | cut -c 1-2)
|
||||
printf '%s+neko:%s.%s' "${UPSTREAM_VERSION}" "${COMMIT_DATE}" "${COMMIT_HASH_INITIAL}"
|
||||
}
|
||||
|
|
4
neko/volume/.gitignore
vendored
Normal file
4
neko/volume/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
30
package.json
30
package.json
|
@ -6,36 +6,43 @@
|
|||
"type": "git",
|
||||
"url": "https://code.naskya.net/naskya/firefish"
|
||||
},
|
||||
"packageManager": "pnpm@8.13.1",
|
||||
"packageManager": "pnpm@8.14.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "pnpm run clean && pnpm run build",
|
||||
"build": "pnpm --filter firefish-js run build && pnpm --filter !firefish-js -r --parallel run build && pnpm run gulp",
|
||||
"start": "pnpm --filter backend run start",
|
||||
"start:container": "pnpm run gulp && pnpm run migrate && pnpm run start",
|
||||
"init": "pnpm run migrate",
|
||||
"migrate": "pnpm --filter backend run migrate",
|
||||
"revertmigration": "pnpm --filter backend run revertmigration",
|
||||
"migrateandstart": "pnpm run migrate && pnpm run start",
|
||||
"gulp": "gulp build",
|
||||
"watch": "pnpm run dev",
|
||||
"dev": "pnpm node ./scripts/dev.mjs",
|
||||
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
||||
"lint": "pnpm -r --parallel run lint",
|
||||
"debug": "pnpm run build:debug && pnpm run start",
|
||||
"build:debug": "pnpm -r --parallel run build:debug && pnpm run gulp",
|
||||
"build:debug": "pnpm clean && pnpm --filter firefish-js run build:types && pnpm -r --parallel run build:debug && pnpm run gulp",
|
||||
"format": "pnpm -r --parallel run format",
|
||||
"clean": "pnpm node ./scripts/clean.mjs",
|
||||
"clean-all": "pnpm node ./scripts/clean-all.mjs",
|
||||
"clean": "pnpm node ./scripts/clean-built.mjs",
|
||||
"clean-cargo": "pnpm node ./scripts/clean-cargo.mjs",
|
||||
"clean-npm": "pnpm node ./scripts/clean-npm.mjs",
|
||||
"clean-all": "pnpm run clean && pnpm run claen-cargo && pnpm run clean-npm",
|
||||
"cleanall": "pnpm run clean-all"
|
||||
},
|
||||
"resolutions": {
|
||||
"chokidar": "3.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.10.2",
|
||||
"@bull-board/ui": "5.10.2",
|
||||
"@bull-board/api": "5.11.0",
|
||||
"@bull-board/ui": "5.11.0",
|
||||
"@napi-rs/cli": "2.17.0",
|
||||
"@tensorflow/tfjs": "4.15.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"seedrandom": "3.0.5"
|
||||
},
|
||||
|
@ -47,17 +54,12 @@
|
|||
"@biomejs/cli-linux-x64": "1.4.1",
|
||||
"@types/gulp": "4.0.17",
|
||||
"@types/gulp-rename": "2.0.6",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/node": "20.10.7",
|
||||
"add": "2.0.6",
|
||||
"cross-env": "7.0.3",
|
||||
"execa": "8.0.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"install-peers": "^1.0.4",
|
||||
"pnpm": "8.13.1",
|
||||
"pnpm": "8.14.0",
|
||||
"typescript": "5.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "pnpm node --dns-result-order=ipv4first ./built/index.js",
|
||||
"start": "pnpm node ./built/index.js",
|
||||
"start:test": "NODE_ENV=test pnpm node ./built/index.js",
|
||||
"migrate": "pnpm run migrate:typeorm && pnpm run migrate:cargo",
|
||||
"migrate:typeorm": "typeorm migration:run -d ormconfig.js",
|
||||
|
@ -24,9 +24,9 @@
|
|||
"@tensorflow/tfjs-node": "4.15.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.10.2",
|
||||
"@bull-board/koa": "5.10.2",
|
||||
"@bull-board/ui": "5.10.2",
|
||||
"@bull-board/api": "5.11.0",
|
||||
"@bull-board/koa": "5.11.0",
|
||||
"@bull-board/ui": "5.11.0",
|
||||
"@discordapp/twemoji": "^15.0.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@koa/multer": "3.0.2",
|
||||
|
@ -41,25 +41,26 @@
|
|||
"ajv": "8.12.0",
|
||||
"archiver": "6.0.1",
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "2.1527.0",
|
||||
"axios": "1.6.3",
|
||||
"aws-sdk": "2.1531.0",
|
||||
"axios": "1.6.5",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bull": "4.12.0",
|
||||
"cacheable-lookup": "TheEssem/cacheable-lookup",
|
||||
"cbor-x": "1.5.6",
|
||||
"cbor-x": "1.5.7",
|
||||
"chalk": "5.3.0",
|
||||
"chalk-template": "1.1.0",
|
||||
"chokidar": "3.5.3",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "3.0.6",
|
||||
"date-fns": "3.1.0",
|
||||
"decompress": "4.2.1",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"deepl-node": "1.11.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.7.0",
|
||||
"file-type": "19.0.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"firefish-js": "workspace:*",
|
||||
"got": "14.0.0",
|
||||
|
@ -73,7 +74,7 @@
|
|||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "10.9.0",
|
||||
"koa": "2.14.2",
|
||||
"koa": "2.15.0",
|
||||
"koa-body": "6.0.1",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
"koa-favicon": "2.1.0",
|
||||
|
@ -91,7 +92,7 @@
|
|||
"native-utils": "link:native-utils",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.7",
|
||||
"nodemailer": "6.9.8",
|
||||
"nsfwjs": "2.4.2",
|
||||
"opencc-js": "1.0.5",
|
||||
"os-utils": "0.0.14",
|
||||
|
@ -124,7 +125,7 @@
|
|||
"tesseract.js": "5.0.4",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"typeorm": "0.3.17",
|
||||
"typeorm": "0.3.19",
|
||||
"ulid": "2.3.0",
|
||||
"uuid": "9.0.1",
|
||||
"web-push": "3.6.6",
|
||||
|
@ -133,7 +134,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "0.1.63",
|
||||
"@swc/core": "1.3.101",
|
||||
"@swc/core": "1.3.102",
|
||||
"@types/adm-zip": "0.5.5",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
|
@ -151,7 +152,7 @@
|
|||
"@types/koa__cors": "5.0.0",
|
||||
"@types/koa__multer": "2.0.7",
|
||||
"@types/koa__router": "12.0.4",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/node": "20.10.7",
|
||||
"@types/node-fetch": "2.6.10",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/oauth": "0.9.4",
|
||||
|
|
|
@ -379,3 +379,4 @@ export const iso639Regional = {
|
|||
};
|
||||
|
||||
export const langmap = Object.assign({}, langmapNoRegion, iso639Regional);
|
||||
export type Language = keyof typeof langmap;
|
||||
|
|
88
packages/backend/src/misc/translate.ts
Normal file
88
packages/backend/src/misc/translate.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import fetch from "node-fetch";
|
||||
import { Converter } from "opencc-js";
|
||||
import { getAgentByUrl } from "@/misc/fetch.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import type { Language } from "@/misc/langmap";
|
||||
import * as deepl from "deepl-node";
|
||||
|
||||
function convertChinese(convert: boolean, src: string) {
|
||||
if (!convert) return src;
|
||||
const converter = Converter({ from: "cn", to: "twp" });
|
||||
return converter(src);
|
||||
}
|
||||
|
||||
function stem(lang: Language): string {
|
||||
let toReturn = lang as string;
|
||||
if (toReturn.includes("-")) toReturn = toReturn.split("-")[0];
|
||||
if (toReturn.includes("_")) toReturn = toReturn.split("_")[0];
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
export default async function (
|
||||
text: string,
|
||||
from: Language | null,
|
||||
to: Language,
|
||||
) {
|
||||
const instance = await fetchMeta();
|
||||
|
||||
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
|
||||
throw Error("No translator is set up on this server.");
|
||||
}
|
||||
|
||||
const source = from == null ? null : stem(from);
|
||||
const target = stem(to);
|
||||
|
||||
if (instance.libreTranslateApiUrl != null) {
|
||||
const jsonBody = {
|
||||
q: text,
|
||||
source: source ?? "auto",
|
||||
target,
|
||||
format: "text",
|
||||
api_key: instance.libreTranslateApiKey ?? "",
|
||||
};
|
||||
|
||||
const url = new URL(instance.libreTranslateApiUrl);
|
||||
if (url.pathname.endsWith("/")) {
|
||||
url.pathname = url.pathname.slice(0, -1);
|
||||
}
|
||||
if (!url.pathname.endsWith("/translate")) {
|
||||
url.pathname += "/translate";
|
||||
}
|
||||
const res = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(jsonBody),
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
detectedLanguage?: {
|
||||
confidence: number;
|
||||
language: string;
|
||||
};
|
||||
translatedText: string;
|
||||
};
|
||||
|
||||
return {
|
||||
sourceLang: source ?? json.detectedLanguage?.language,
|
||||
text: convertChinese(
|
||||
["zh-hant", "zh-TW"].includes(to),
|
||||
json.translatedText,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const deeplTranslator = new deepl.Translator(instance.deeplAuthKey ?? "");
|
||||
const result = await deeplTranslator.translateText(
|
||||
text,
|
||||
source as deepl.SourceLanguageCode | null,
|
||||
(target === "en" ? to : target) as deepl.TargetLanguageCode,
|
||||
);
|
||||
|
||||
return {
|
||||
sourceLang: source ?? result.detectedSourceLang,
|
||||
text: convertChinese(["zh-hant", "zh-TW"].includes(to), result.text),
|
||||
};
|
||||
}
|
|
@ -61,6 +61,11 @@ export class Meta {
|
|||
})
|
||||
public disableGlobalTimeline: boolean;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public enableGuestTimeline: boolean;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 256,
|
||||
default: "⭐",
|
||||
|
|
|
@ -10,8 +10,6 @@ import type { IncomingMessage } from "http";
|
|||
import type { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||
import type { UserPublickey } from "@/models/entities/user-publickey.js";
|
||||
import { verify } from "node:crypto";
|
||||
import { toSingle } from "@/prelude/array.js";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
export async function hasSignature(req: IncomingMessage): Promise<string> {
|
||||
const meta = await fetchMeta();
|
||||
|
@ -158,20 +156,3 @@ export function verifySignature(
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyDigest(
|
||||
body: string,
|
||||
digest: string | string[] | undefined,
|
||||
): boolean {
|
||||
digest = toSingle(digest);
|
||||
if (
|
||||
body == null ||
|
||||
digest == null ||
|
||||
!digest.toLowerCase().startsWith("sha-256=")
|
||||
)
|
||||
return false;
|
||||
|
||||
return (
|
||||
createHash("sha256").update(body).digest("base64") === digest.substring(8)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -129,6 +129,12 @@ export async function createNote(
|
|||
throw new Error(`unexpected schema of note.id: ${note.id}`);
|
||||
}
|
||||
|
||||
// ChatMessage only have id
|
||||
// TODO: split into a separate validate function
|
||||
if (note.type === "ChatMessage") {
|
||||
note.url = note.id;
|
||||
}
|
||||
|
||||
const url = getOneApHrefNullable(note.url);
|
||||
|
||||
if (url && !url.startsWith("https://")) {
|
||||
|
@ -183,7 +189,9 @@ export async function createNote(
|
|||
}
|
||||
}
|
||||
|
||||
let isTalk = note._misskey_talk && visibility === "specified";
|
||||
let isTalk =
|
||||
(note.type === "ChatMessage" || note._misskey_talk) &&
|
||||
visibility === "specified";
|
||||
|
||||
const apMentions = await extractApMentions(note.tag);
|
||||
const apHashtags = await extractApHashtags(note.tag);
|
||||
|
|
|
@ -49,6 +49,10 @@ export const renderActivity = (x: any): IActivity | null => {
|
|||
fedibird: "http://fedibird.com/ns#",
|
||||
// vcard
|
||||
vcard: "http://www.w3.org/2006/vcard/ns#",
|
||||
// ChatMessage
|
||||
litepub: "http://litepub.social/ns#",
|
||||
ChatMessage: "litepub:ChatMessage",
|
||||
directMessage: "litepub:directMessage",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -115,6 +115,7 @@ export const validPost = [
|
|||
"Page",
|
||||
"Video",
|
||||
"Event",
|
||||
"ChatMessage", // TODO: move it to vaildMessage
|
||||
];
|
||||
|
||||
export const isPost = (object: IObject): object is IPost =>
|
||||
|
@ -130,6 +131,7 @@ export interface IPost extends IObject {
|
|||
| "Image"
|
||||
| "Page"
|
||||
| "Video"
|
||||
| "ChatMessage" // TODO: move it to IChatMessage
|
||||
| "Event";
|
||||
source?: {
|
||||
content: string;
|
||||
|
|
|
@ -23,7 +23,6 @@ import { getUserKeypair } from "@/misc/keypair-store.js";
|
|||
import {
|
||||
checkFetch,
|
||||
getSignatureUser,
|
||||
verifyDigest,
|
||||
} from "@/remote/activitypub/check-fetch.js";
|
||||
import { getInstanceActor } from "@/services/instance-actor.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
|
@ -35,6 +34,9 @@ import Outbox, { packActivity } from "./activitypub/outbox.js";
|
|||
import { serverLogger } from "./index.js";
|
||||
import config from "@/config/index.js";
|
||||
import Koa from "koa";
|
||||
import * as crypto from "node:crypto";
|
||||
import { inspect } from "node:util";
|
||||
import type { IActivity } from "@/remote/activitypub/type.js";
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
@ -43,27 +45,94 @@ const router = new Router();
|
|||
|
||||
function inbox(ctx: Router.RouterContext) {
|
||||
if (ctx.req.headers.host !== config.host) {
|
||||
serverLogger.warn("inbox: Invalid Host");
|
||||
ctx.status = 400;
|
||||
ctx.message = "Invalid Host";
|
||||
return;
|
||||
}
|
||||
|
||||
let signature;
|
||||
let signature: httpSignature.IParsedSignature;
|
||||
|
||||
try {
|
||||
signature = httpSignature.parseRequest(ctx.req, {
|
||||
headers: ["(request-target)", "digest", "host", "date"],
|
||||
});
|
||||
} catch (e) {
|
||||
serverLogger.warn(`inbox: signature parse error: ${inspect(e)}`);
|
||||
ctx.status = 401;
|
||||
|
||||
if (e instanceof Error) {
|
||||
if (e.name === "ExpiredRequestError")
|
||||
ctx.message = "Expired Request Error";
|
||||
if (e.name === "MissingHeaderError")
|
||||
ctx.message = "Missing Required Header";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verifyDigest(ctx.request.rawBody, ctx.headers.digest)) {
|
||||
// Validate signature algorithm
|
||||
if (
|
||||
!signature.algorithm
|
||||
.toLowerCase()
|
||||
.match(/^((dsa|rsa|ecdsa)-(sha256|sha384|sha512)|ed25519-sha512|hs2019)$/)
|
||||
) {
|
||||
serverLogger.warn(
|
||||
`inbox: invalid signature algorithm ${signature.algorithm}`,
|
||||
);
|
||||
ctx.status = 401;
|
||||
ctx.message = "Invalid Signature Algorithm";
|
||||
return;
|
||||
|
||||
// hs2019
|
||||
// keyType=ED25519 => ed25519-sha512
|
||||
// keyType=other => (keyType)-sha256
|
||||
}
|
||||
|
||||
// Validate digest header
|
||||
const digest = ctx.req.headers.digest;
|
||||
|
||||
if (typeof digest !== "string") {
|
||||
serverLogger.warn(
|
||||
"inbox: zero or more than one digest header(s) are present",
|
||||
);
|
||||
ctx.status = 401;
|
||||
ctx.message = "Invalid Digest Header";
|
||||
return;
|
||||
}
|
||||
|
||||
processInbox(ctx.request.body, signature);
|
||||
const match = digest.match(/^([0-9A-Za-z-]+)=(.+)$/);
|
||||
|
||||
if (match == null) {
|
||||
serverLogger.warn("inbox: unrecognized digest header");
|
||||
ctx.status = 401;
|
||||
ctx.message = "Invalid Digest Header";
|
||||
return;
|
||||
}
|
||||
|
||||
const digestAlgo = match[1];
|
||||
const expectedDigest = match[2];
|
||||
|
||||
if (digestAlgo.toUpperCase() !== "SHA-256") {
|
||||
serverLogger.warn("inbox: unsupported digest algorithm");
|
||||
ctx.status = 401;
|
||||
ctx.message = "Unsupported Digest Algorithm";
|
||||
return;
|
||||
}
|
||||
|
||||
const actualDigest = crypto
|
||||
.createHash("sha256")
|
||||
.update(ctx.request.rawBody)
|
||||
.digest("base64");
|
||||
|
||||
if (expectedDigest !== actualDigest) {
|
||||
serverLogger.warn("inbox: Digest Mismatch");
|
||||
ctx.status = 401;
|
||||
ctx.message = "Digest Missmatch";
|
||||
return;
|
||||
}
|
||||
|
||||
processInbox(ctx.request.body as IActivity, signature);
|
||||
|
||||
ctx.status = 202;
|
||||
}
|
||||
|
|
|
@ -296,6 +296,7 @@ import * as ep___customSplashIcons from "./endpoints/custom-splash-icons.js";
|
|||
import * as ep___latestVersion from "./endpoints/latest-version.js";
|
||||
import * as ep___patrons from "./endpoints/patrons.js";
|
||||
import * as ep___release from "./endpoints/release.js";
|
||||
import * as ep___release_translate from "./endpoints/release/translate.js";
|
||||
import * as ep___promo_read from "./endpoints/promo/read.js";
|
||||
import * as ep___requestResetPassword from "./endpoints/request-reset-password.js";
|
||||
import * as ep___resetDb from "./endpoints/reset-db.js";
|
||||
|
@ -655,6 +656,7 @@ const eps = [
|
|||
["latest-version", ep___latestVersion],
|
||||
["patrons", ep___patrons],
|
||||
["release", ep___release],
|
||||
["release/translate", ep___release_translate],
|
||||
["promo/read", ep___promo_read],
|
||||
["request-reset-password", ep___requestResetPassword],
|
||||
["reset-db", ep___resetDb],
|
||||
|
|
|
@ -460,7 +460,7 @@ export const paramDef = {
|
|||
required: [],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
return {
|
||||
|
@ -479,6 +479,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
disableLocalTimeline: instance.disableLocalTimeline,
|
||||
disableRecommendedTimeline: instance.disableRecommendedTimeline,
|
||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||
enableGuestTimeline: instance.enableGuestTimeline,
|
||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
|
|
|
@ -17,6 +17,7 @@ export const paramDef = {
|
|||
disableLocalTimeline: { type: "boolean", nullable: true },
|
||||
disableRecommendedTimeline: { type: "boolean", nullable: true },
|
||||
disableGlobalTimeline: { type: "boolean", nullable: true },
|
||||
enableGuestTimeline: { type: "boolean", nullable: true },
|
||||
defaultReaction: { type: "string", nullable: true },
|
||||
recommendedInstances: {
|
||||
type: "array",
|
||||
|
@ -216,6 +217,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
||||
}
|
||||
|
||||
if (typeof ps.enableGuestTimeline === "boolean") {
|
||||
set.enableGuestTimeline = ps.enableGuestTimeline;
|
||||
}
|
||||
|
||||
if (typeof ps.defaultReaction === "string") {
|
||||
set.defaultReaction = ps.defaultReaction;
|
||||
}
|
||||
|
|
|
@ -111,6 +111,11 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableGuestTimeline: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
driveCapacityPerLocalUserMb: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
|
@ -432,6 +437,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
disableLocalTimeline: instance.disableLocalTimeline,
|
||||
disableRecommendedTimeline: instance.disableRecommendedTimeline,
|
||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||
enableGuestTimeline: instance.enableGuestTimeline,
|
||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
|
@ -506,8 +512,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
localTimeLine: !instance.disableLocalTimeline,
|
||||
recommendedTimeline: !instance.disableRecommendedTimeline,
|
||||
globalTimeLine: !instance.disableGlobalTimeline,
|
||||
gusstTimeline: instance.enableGuestTimeline,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
searchFilters: config.meilisearch ? true : false,
|
||||
searchFilters: false, // TODO: implement search filters
|
||||
hcaptcha: instance.enableHcaptcha,
|
||||
recaptcha: instance.enableRecaptcha,
|
||||
objectStorage: instance.useObjectStorage,
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import { URLSearchParams } from "node:url";
|
||||
import fetch from "node-fetch";
|
||||
import config from "@/config/index.js";
|
||||
import { Converter } from "opencc-js";
|
||||
import { getAgentByUrl } from "@/misc/fetch.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import translate from "@/misc/translate.js";
|
||||
import type { Language } from "@/misc/langmap";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -26,6 +22,11 @@ export const meta = {
|
|||
code: "NO_SUCH_NOTE",
|
||||
id: "bea9b03f-36e0-49c5-a4db-627a029f8971",
|
||||
},
|
||||
noteTextIsNull: {
|
||||
message: "The text of this note is null.",
|
||||
code: "NOTE_TEXT_IS_NULL",
|
||||
id: "c2794117-1a8d-4fe5-8925-0eca24ba47d0",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -38,13 +39,6 @@ export const paramDef = {
|
|||
required: ["noteId", "targetLang"],
|
||||
} as const;
|
||||
|
||||
function convertChinese(convert: boolean, src: string) {
|
||||
if (!convert) return src;
|
||||
|
||||
const converter = Converter({ from: "cn", to: "twp" });
|
||||
return converter(src);
|
||||
}
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId, user).catch((err) => {
|
||||
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24")
|
||||
|
@ -53,89 +47,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
if (note.text == null) {
|
||||
return 204;
|
||||
throw new ApiError(meta.errors.noteTextIsNull);
|
||||
}
|
||||
|
||||
const instance = await fetchMeta();
|
||||
|
||||
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
|
||||
return 204; // TODO: 良い感じのエラー返す
|
||||
}
|
||||
|
||||
let targetLang = ps.targetLang;
|
||||
if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
|
||||
if (targetLang.includes("_")) targetLang = targetLang.split("_")[0];
|
||||
|
||||
if (instance.libreTranslateApiUrl != null) {
|
||||
const jsonBody = {
|
||||
q: note.text,
|
||||
source: "auto",
|
||||
target: targetLang,
|
||||
format: "text",
|
||||
api_key: instance.libreTranslateApiKey ?? "",
|
||||
};
|
||||
|
||||
const url = new URL(instance.libreTranslateApiUrl);
|
||||
if (url.pathname.endsWith("/")) {
|
||||
url.pathname = url.pathname.slice(0, -1);
|
||||
}
|
||||
if (!url.pathname.endsWith("/translate")) {
|
||||
url.pathname += "/translate";
|
||||
}
|
||||
const res = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(jsonBody),
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
detectedLanguage?: {
|
||||
confidence: number;
|
||||
language: string;
|
||||
};
|
||||
translatedText: string;
|
||||
};
|
||||
|
||||
return {
|
||||
sourceLang: json.detectedLanguage?.language,
|
||||
text: convertChinese(ps.targetLang === "zh-TW", json.translatedText),
|
||||
};
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append("auth_key", instance.deeplAuthKey ?? "");
|
||||
params.append("text", note.text);
|
||||
params.append("target_lang", targetLang);
|
||||
|
||||
const endpoint = instance.deeplIsPro
|
||||
? "https://api.deepl.com/v2/translate"
|
||||
: "https://api-free.deepl.com/v2/translate";
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"User-Agent": config.userAgent,
|
||||
Accept: "application/json, */*",
|
||||
},
|
||||
body: params,
|
||||
// TODO
|
||||
//timeout: 10000,
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
translations: {
|
||||
detected_source_language: string;
|
||||
text: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
return {
|
||||
sourceLang: json.translations[0].detected_source_language,
|
||||
text: convertChinese(ps.targetLang === "zh-TW", json.translations[0].text),
|
||||
};
|
||||
return translate(
|
||||
note.text,
|
||||
note.lang as Language | null,
|
||||
ps.targetLang as Language,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -48,6 +48,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
for (const note of renotes) {
|
||||
deleteNote(await Users.findOneByOrFail({ id: user.id }), note);
|
||||
// Only renotes should be deleted, not quotes
|
||||
if (!note.text) {
|
||||
deleteNote(await Users.findOneByOrFail({ id: user.id }), note);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import define from "@/server/api/define.js";
|
||||
import { redisClient } from "@/db/redis.js";
|
||||
import * as fs from "node:fs";
|
||||
// import { redisClient } from "@/db/redis.js";
|
||||
import * as fs from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
|
@ -25,7 +25,7 @@ export const paramDef = {
|
|||
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
const patrons = JSON.parse(
|
||||
fs.readFileSync(`${_dirname}/../../../../../../patrons.json`, "utf-8"),
|
||||
await fs.readFile(`${_dirname}/../../../../../../patrons.json`, "utf-8"),
|
||||
);
|
||||
return {
|
||||
patrons: patrons.patrons,
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import * as fs from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import define from "@/server/api/define.js";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
export const meta = {
|
||||
tags: ["meta"],
|
||||
description: "Get release notes from Codeberg",
|
||||
description: "Get changelog",
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: false,
|
||||
|
@ -14,15 +21,11 @@ export const paramDef = {
|
|||
required: [],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async () => {
|
||||
let release;
|
||||
|
||||
await fetch(
|
||||
"https://git.joinfirefish.org/firefish/firefish/-/raw/develop/release.json",
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
release = data;
|
||||
});
|
||||
return release;
|
||||
});
|
||||
export default define(meta, paramDef, async () => ({
|
||||
version: config.version,
|
||||
notes: await fs.readFile(
|
||||
`${_dirname}/../../../../../../neko/volume/CHANGELOG`,
|
||||
"utf-8",
|
||||
),
|
||||
screenshots: [],
|
||||
}));
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import * as fs from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import define from "@/server/api/define.js";
|
||||
import translate from "@/misc/translate.js";
|
||||
import type { Language } from "@/misc/langmap.js";
|
||||
import RE2 from "re2";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
export const meta = {
|
||||
tags: ["meta"],
|
||||
description: "Translate changelog",
|
||||
|
||||
requireCredential: true,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
targetLang: { type: "string" },
|
||||
},
|
||||
required: ["targetLang"],
|
||||
} as const;
|
||||
|
||||
async function translateCommitMsg(msg: string, targetLang: Language) {
|
||||
const regex = new RE2(/^(.*) (\(by .*\))$/);
|
||||
const matches = regex.match(msg);
|
||||
|
||||
if (matches == null) return msg;
|
||||
|
||||
if (targetLang.startsWith("ja")) {
|
||||
const prefixes = {
|
||||
chore: "雑務",
|
||||
dev: "開発",
|
||||
docker: "Docker",
|
||||
docs: "ドキュメント",
|
||||
feat: "新機能",
|
||||
fix: "修正",
|
||||
hotfix: "緊急修正",
|
||||
locale: "翻訳",
|
||||
perf: "パフォーマンス",
|
||||
refactor: "再設計",
|
||||
style: "体裁",
|
||||
};
|
||||
|
||||
for (const [prefix, translatedPrefix] of Object.entries(prefixes)) {
|
||||
if (msg.startsWith(`${prefix}:`))
|
||||
return `${translatedPrefix}: ${
|
||||
(await translate(matches[1].split(":")[1].trim(), "en", "ja")).text
|
||||
} ${matches[2]}`;
|
||||
if (msg.startsWith(`${prefix} (minor):`))
|
||||
return `${translatedPrefix}(小規模): ${
|
||||
(await translate(matches[1].split(":")[1].trim(), "en", "ja")).text
|
||||
} ${matches[2]}`;
|
||||
}
|
||||
}
|
||||
return `${(await translate(matches[1], "en", targetLang)).text} ${
|
||||
matches[2]
|
||||
}`;
|
||||
}
|
||||
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
const releaseNotes = (
|
||||
await fs.readFile(
|
||||
`${_dirname}/../../../../../../../neko/volume/CHANGELOG`,
|
||||
"utf-8",
|
||||
)
|
||||
)
|
||||
.trim()
|
||||
.split("\n");
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const msg of releaseNotes)
|
||||
promises.push(translateCommitMsg(msg, ps.targetLang as Language));
|
||||
|
||||
return await Promise.all(promises);
|
||||
});
|
|
@ -23,6 +23,8 @@ export default class extends Channel {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!meta.enableGuestTimeline && this.user == null) return;
|
||||
|
||||
this.withReplies = params != null ? !!params.withReplies : true;
|
||||
|
||||
// Subscribe events
|
||||
|
|
|
@ -22,6 +22,8 @@ export default class extends Channel {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!meta.enableGuestTimeline && this.user == null) return;
|
||||
|
||||
this.withReplies = params != null ? !!params.withReplies : true;
|
||||
|
||||
// Subscribe events
|
||||
|
|
|
@ -22,6 +22,8 @@ import renderNote from "@/remote/activitypub/renderer/note.js";
|
|||
import renderCreate from "@/remote/activitypub/renderer/create.js";
|
||||
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||
import { deliver } from "@/queue/index.js";
|
||||
import { toPuny } from "@/misc/convert-host.js";
|
||||
import { Instances } from "@/models/index.js";
|
||||
|
||||
export async function createMessage(
|
||||
user: { id: User["id"]; host: User["host"] },
|
||||
|
@ -121,6 +123,9 @@ export async function createMessage(
|
|||
Users.isLocalUser(user) &&
|
||||
Users.isRemoteUser(recipientUser)
|
||||
) {
|
||||
const instance = await Instances.findOneBy({
|
||||
host: toPuny(recipientUser.host),
|
||||
});
|
||||
const note = {
|
||||
id: message.id,
|
||||
createdAt: message.createdAt,
|
||||
|
@ -138,10 +143,43 @@ export async function createMessage(
|
|||
),
|
||||
} as Note;
|
||||
|
||||
const activity = renderActivity(
|
||||
renderCreate(await renderNote(note, false, true), note),
|
||||
let renderedNote: Record<string, unknown> = await renderNote(
|
||||
note,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
// TODO: For pleroma and its fork instances, the actor will have a boolean "capabilities": { acceptsChatMessages: boolean } property. May use that instead of checking instance.softwareName. https://kazv.moe/objects/ca5c0b88-88ce-48a7-bf88-54d45f6ce781
|
||||
// ChatMessage document from Pleroma: https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
|
||||
// Note: LitePub has been stalled since 2019-06-29 and is incomplete as a specification
|
||||
if (
|
||||
instance?.softwareName &&
|
||||
["akkoma", "pleroma", "lemmy"].includes(
|
||||
instance.softwareName.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
const tmp_note = renderedNote;
|
||||
renderedNote = {
|
||||
type: "ChatMessage",
|
||||
attributedTo: tmp_note.attributedTo,
|
||||
content: tmp_note.content,
|
||||
id: tmp_note.id,
|
||||
published: tmp_note.published,
|
||||
to: tmp_note.to,
|
||||
tag: tmp_note.tag,
|
||||
cc: [],
|
||||
};
|
||||
// A recently fixed bug, empty arrays will be rejected by pleroma
|
||||
if (
|
||||
Array.isArray(tmp_note.attachment) &&
|
||||
tmp_note.attachment.length !== 0
|
||||
) {
|
||||
renderedNote.attachment = tmp_note.attachment;
|
||||
}
|
||||
}
|
||||
|
||||
const activity = renderActivity(renderCreate(renderedNote, note));
|
||||
|
||||
deliver(user, activity, recipientUser.inbox);
|
||||
}
|
||||
return messageObj;
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
"format": "pnpm biome format * --write && pnpm prettier --write '**/*.{scss,vue}' --cache --cache-strategy metadata"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-sets/eslint-config-vue3": "^5.10.0",
|
||||
"@eslint-sets/eslint-config-vue3": "^5.11.0",
|
||||
"@eslint-sets/eslint-config-vue3-ts": "^3.3.0",
|
||||
"@phosphor-icons/web": "^2.0.3",
|
||||
"@rollup/plugin-alias": "5.1.0",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/pluginutils": "5.1.0",
|
||||
"@syuilo/aiscript": "0.16.0",
|
||||
"@syuilo/aiscript": "0.17.0",
|
||||
"@types/autosize": "^4.0.3",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/gulp": "4.0.17",
|
||||
|
@ -31,9 +31,9 @@
|
|||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/uuid": "9.0.7",
|
||||
"@vitejs/plugin-vue": "5.0.0",
|
||||
"@vue/compiler-sfc": "3.3.13",
|
||||
"@vue/runtime-core": "3.3.13",
|
||||
"@vitejs/plugin-vue": "5.0.2",
|
||||
"@vue/compiler-sfc": "3.4.5",
|
||||
"@vue/runtime-core": "3.4.5",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "6.0.1",
|
||||
"blurhash": "2.0.5",
|
||||
|
@ -47,7 +47,7 @@
|
|||
"city-timezones": "1.2.1",
|
||||
"compare-versions": "6.1.0",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "3.0.6",
|
||||
"date-fns": "3.1.0",
|
||||
"emojilib": "^3.0.11",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-file-progress": "1.3.0",
|
||||
|
@ -68,9 +68,9 @@
|
|||
"prettier": "3.1.1",
|
||||
"prismjs": "1.29.0",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.9.1",
|
||||
"rollup": "4.9.4",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.69.5",
|
||||
"sass": "1.69.7",
|
||||
"seedrandom": "3.0.5",
|
||||
"stringz": "2.1.0",
|
||||
"swiper": "11.0.5",
|
||||
|
@ -83,10 +83,10 @@
|
|||
"typescript": "5.3.3",
|
||||
"unicode-emoji-json": "0.4.0",
|
||||
"uuid": "9.0.1",
|
||||
"vite": "5.0.10",
|
||||
"vite": "5.0.11",
|
||||
"vite-plugin-compression": "0.5.1",
|
||||
"vue": "3.3.13",
|
||||
"vue-draggable-plus": "^0.3.3",
|
||||
"vue": "3.4.5",
|
||||
"vue-draggable-plus": "^0.3.4",
|
||||
"vue-plyr": "^7.0.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2"
|
||||
}
|
||||
|
|
|
@ -12,10 +12,6 @@ import { reloadChannel, unisonReload } from "@/scripts/unison-reload";
|
|||
|
||||
export type Account = firefish.entities.MeDetailed;
|
||||
|
||||
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
|
||||
export const iAmAdmin = $i?.isAdmin;
|
||||
export const iAmEmojiMod = iAmModerator || $i?.emojiModPerm !== "unauthorized";
|
||||
|
||||
export async function signout() {
|
||||
waiting();
|
||||
localStorage.removeItem("account");
|
||||
|
@ -59,7 +55,7 @@ export async function signout() {
|
|||
export async function getAccounts(): Promise<
|
||||
{ id: Account["id"]; token: Account["token"] }[]
|
||||
> {
|
||||
return (await get("accounts")) || [];
|
||||
return (await get("accounts")) ?? [];
|
||||
}
|
||||
|
||||
export async function addAccount(id: Account["id"], token: Account["token"]) {
|
||||
|
|
|
@ -59,7 +59,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const uiWindow = ref<InstanceType<typeof XWindow>>();
|
||||
const comment = ref(props.initialComment || "");
|
||||
const comment = ref(props.initialComment ?? "");
|
||||
|
||||
function send() {
|
||||
os.apiWithDialog(
|
||||
|
|
|
@ -268,7 +268,7 @@ function exec() {
|
|||
} else if (props.type === "hashtag") {
|
||||
if (!props.q || props.q === "") {
|
||||
hashtags.value = JSON.parse(
|
||||
localStorage.getItem("hashtags") || "[]",
|
||||
localStorage.getItem("hashtags") ?? "[]",
|
||||
);
|
||||
fetching.value = false;
|
||||
} else {
|
||||
|
|
|
@ -78,14 +78,14 @@ const src = computed(() => {
|
|||
});
|
||||
|
||||
const captcha = computed<Captcha>(
|
||||
() => window[variable.value] || ({} as unknown as Captcha),
|
||||
() => window[variable.value] ?? ({} as Captcha),
|
||||
);
|
||||
|
||||
if (loaded) {
|
||||
available.value = true;
|
||||
} else {
|
||||
(
|
||||
document.getElementById(props.provider) ||
|
||||
document.getElementById(props.provider) ??
|
||||
document.head.appendChild(
|
||||
Object.assign(document.createElement("script"), {
|
||||
async: true,
|
||||
|
|
|
@ -50,7 +50,7 @@ export default defineComponent({
|
|||
|
||||
const renderChildren = () =>
|
||||
props.items.map((item, i) => {
|
||||
if (!slots || !slots.default) return;
|
||||
if (slots == null || slots.default == null) return;
|
||||
|
||||
const el = slots.default({
|
||||
item,
|
||||
|
|
|
@ -65,8 +65,10 @@
|
|||
v-model="inputValue"
|
||||
autofocus
|
||||
:autocomplete="input.autocomplete"
|
||||
:type="input.type == 'search' ? 'search' : input.type || 'text'"
|
||||
:placeholder="input.placeholder || undefined"
|
||||
:type="
|
||||
input.type === 'search' ? 'search' : input.type ?? 'text'
|
||||
"
|
||||
:placeholder="input.placeholder ?? undefined"
|
||||
:style="{
|
||||
width: input.type === 'search' ? '300px' : null,
|
||||
}"
|
||||
|
@ -294,7 +296,7 @@ const okButtonDisabled = computed<boolean>(() => {
|
|||
if (props.input) {
|
||||
if (props.input.minLength) {
|
||||
if (
|
||||
(inputValue.value || inputValue.value === "") &&
|
||||
inputValue.value != null &&
|
||||
(inputValue.value as string).length < props.input.minLength
|
||||
) {
|
||||
disabledReason.value = "charactersBelow";
|
||||
|
|
|
@ -185,7 +185,7 @@ function describe() {
|
|||
},
|
||||
{
|
||||
done: (result) => {
|
||||
if (!result || result.canceled) return;
|
||||
if (result == null || result.canceled) return;
|
||||
const comment = result.result;
|
||||
os.api("drive/files/update", {
|
||||
fileId: props.file.id,
|
||||
|
|
|
@ -363,7 +363,7 @@ function urlUpload() {
|
|||
type: "url",
|
||||
placeholder: i18n.ts.uploadFromUrlDescription,
|
||||
}).then(({ canceled, result: url }) => {
|
||||
if (canceled || !url) return;
|
||||
if (canceled || url == null) return;
|
||||
os.api("drive/files/upload-from-url", {
|
||||
url,
|
||||
folderId: folder.value ? folder.value.id : undefined,
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
.map((e) => ':' + e.name + ':')
|
||||
"
|
||||
@chosen="chosen"
|
||||
>{{ category || i18n.ts.other }}</XSection
|
||||
>{{ category ?? i18n.ts.other }}</XSection
|
||||
>
|
||||
</div>
|
||||
<div v-once class="group">
|
||||
|
@ -423,7 +423,7 @@ function reset() {
|
|||
function getKey(
|
||||
emoji: string | firefish.entities.CustomEmoji | UnicodeEmojiDef,
|
||||
): string {
|
||||
return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`;
|
||||
return typeof emoji === "string" ? emoji : emoji.emoji ?? `:${emoji.name}:`;
|
||||
}
|
||||
|
||||
function chosen(emoji: any, ev?: MouseEvent) {
|
||||
|
@ -450,7 +450,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
|
|||
}
|
||||
|
||||
function paste(event: ClipboardEvent) {
|
||||
const paste = (event.clipboardData || window.clipboardData).getData("text");
|
||||
const paste = (event.clipboardData ?? window.clipboardData).getData("text");
|
||||
if (done(paste)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
<i :class="icon('ph-dots-three-outline')"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="!hideFollowButton && $i != null && $i.id != user.id"
|
||||
v-tooltip="full ? null : `${state} ${user.name || user.username}`"
|
||||
v-if="!hideFollowButton && isSignedIn && $i.id != user.id"
|
||||
v-tooltip="full ? null : `${state} ${user.name ?? user.username}`"
|
||||
class="kpoogebi _button follow-button"
|
||||
:class="{
|
||||
wait,
|
||||
|
@ -19,7 +19,7 @@
|
|||
blocking: isBlocking,
|
||||
}"
|
||||
:disabled="wait"
|
||||
:aria-label="`${state} ${user.name || user.username}`"
|
||||
:aria-label="`${state} ${user.name ?? user.username}`"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
|
@ -66,7 +66,7 @@ import type * as firefish from "firefish-js";
|
|||
import * as os from "@/os";
|
||||
import { useStream } from "@/stream";
|
||||
import { i18n } from "@/i18n";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import { getUserMenu } from "@/scripts/get-user-menu";
|
||||
import { useRouter } from "@/router";
|
||||
import { vibrate } from "@/scripts/vibrate";
|
||||
|
@ -141,7 +141,7 @@ async function onClick() {
|
|||
const { canceled } = await os.confirm({
|
||||
type: "warning",
|
||||
text: i18n.t("unfollowConfirm", {
|
||||
name: props.user.name || props.user.username,
|
||||
name: props.user.name ?? props.user.username,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -25,11 +25,11 @@
|
|||
v-if="form[item].type === 'number'"
|
||||
v-model="values[item]"
|
||||
type="number"
|
||||
:step="form[item].step || 1"
|
||||
:step="form[item].step ?? 1"
|
||||
class="_formBlock"
|
||||
>
|
||||
<template #label
|
||||
><span v-text="form[item].label || item"></span
|
||||
><span v-text="form[item].label ?? item"></span
|
||||
><span v-if="form[item].required === false">
|
||||
({{ i18n.ts.optional }})</span
|
||||
></template
|
||||
|
@ -48,7 +48,7 @@
|
|||
class="_formBlock"
|
||||
>
|
||||
<template #label
|
||||
><span v-text="form[item].label || item"></span
|
||||
><span v-text="form[item].label ?? item"></span
|
||||
><span v-if="form[item].required === false">
|
||||
({{ i18n.ts.optional }})</span
|
||||
></template
|
||||
|
@ -65,7 +65,7 @@
|
|||
class="_formBlock"
|
||||
>
|
||||
<template #label
|
||||
><span v-text="form[item].label || item"></span
|
||||
><span v-text="form[item].label ?? item"></span
|
||||
><span v-if="form[item].required === false">
|
||||
({{ i18n.ts.optional }})</span
|
||||
></template
|
||||
|
@ -79,7 +79,7 @@
|
|||
v-model="values[item]"
|
||||
class="_formBlock"
|
||||
>
|
||||
<span v-text="form[item].label || item"></span>
|
||||
<span v-text="form[item].label ?? item"></span>
|
||||
<template v-if="form[item].description" #caption>{{
|
||||
form[item].description
|
||||
}}</template>
|
||||
|
@ -90,7 +90,7 @@
|
|||
class="_formBlock"
|
||||
>
|
||||
<template #label
|
||||
><span v-text="form[item].label || item"></span
|
||||
><span v-text="form[item].label ?? item"></span
|
||||
><span v-if="form[item].required === false">
|
||||
({{ i18n.ts.optional }})</span
|
||||
></template
|
||||
|
@ -109,7 +109,7 @@
|
|||
class="_formBlock"
|
||||
>
|
||||
<template #label
|
||||
><span v-text="form[item].label || item"></span
|
||||
><span v-text="form[item].label ?? item"></span
|
||||
><span v-if="form[item].required === false">
|
||||
({{ i18n.ts.optional }})</span
|
||||
></template
|
||||
|
@ -132,7 +132,7 @@
|
|||
class="_formBlock"
|
||||
>
|
||||
<template #label
|
||||
><span v-text="form[item].label || item"></span
|
||||
><span v-text="form[item].label ?? item"></span
|
||||
><span v-if="form[item].required === false">
|
||||
({{ i18n.ts.optional }})</span
|
||||
></template
|
||||
|
@ -146,7 +146,7 @@
|
|||
class="_formBlock"
|
||||
@click="form[item].action($event, values)"
|
||||
>
|
||||
<span v-text="form[item].content || item"></span>
|
||||
<span v-text="form[item].content ?? item"></span>
|
||||
</MkButton>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<span class="host">{{ instance.name ?? instance.host }}</span>
|
||||
<span class="sub _monospace"
|
||||
><b>{{ instance.host }}</b> /
|
||||
{{ instance.softwareName || "?" }}
|
||||
{{ instance.softwareName ?? "?" }}
|
||||
{{ instance.softwareVersion }}</span
|
||||
>
|
||||
</div>
|
||||
|
|
|
@ -33,7 +33,7 @@ const ticker = ref<HTMLElement | null>(null);
|
|||
|
||||
// if no instance data is given, this is for the local instance
|
||||
const instance = props.instance ?? {
|
||||
faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico",
|
||||
faviconUrl: Instance.faviconUrl ?? Instance.iconUrl ?? "/favicon.ico",
|
||||
name: instanceName,
|
||||
themeColor: (
|
||||
document.querySelector(
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<span class="main">
|
||||
<span class="username">@{{ username }}</span>
|
||||
<span
|
||||
v-if="host != localHost || defaultStore.state.showFullAcct"
|
||||
v-if="host !== localHost || defaultStore.state.showFullAcct"
|
||||
class="host"
|
||||
>@{{ toUnicode(host) }}</span
|
||||
>
|
||||
|
@ -35,9 +35,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { toUnicode } from "punycode";
|
||||
import {} from "vue";
|
||||
import { host as localHost } from "@/config";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import { defaultStore } from "@/store";
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -53,7 +52,7 @@ const canonical =
|
|||
const url = `/${canonical}`;
|
||||
|
||||
const isMe =
|
||||
$i &&
|
||||
isSignedIn &&
|
||||
`@${props.username}@${toUnicode(props.host)}` ===
|
||||
`@${$i.username}@${toUnicode(localHost)}`.toLowerCase();
|
||||
</script>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<template v-for="item in items2">
|
||||
<div v-if="item === null" class="divider"></div>
|
||||
<span v-else-if="item.type === 'label'" class="label item">
|
||||
<span :style="item.textStyle || ''">{{
|
||||
<span :style="item.textStyle ?? ''">{{
|
||||
item.text
|
||||
}}</span>
|
||||
</span>
|
||||
|
@ -50,7 +50,7 @@
|
|||
class="avatar"
|
||||
disable-link
|
||||
/>
|
||||
<span :style="item.textStyle || ''">{{
|
||||
<span :style="item.textStyle ?? ''">{{
|
||||
item.text
|
||||
}}</span>
|
||||
<span
|
||||
|
@ -76,7 +76,7 @@
|
|||
v-if="item.icon"
|
||||
:class="icon(`${item.icon} ph-fw`)"
|
||||
></i>
|
||||
<span :style="item.textStyle || ''">{{
|
||||
<span :style="item.textStyle ?? ''">{{
|
||||
item.text
|
||||
}}</span>
|
||||
<span
|
||||
|
@ -121,7 +121,7 @@
|
|||
v-model="item.ref"
|
||||
:disabled="item.disabled"
|
||||
class="form-switch"
|
||||
:style="item.textStyle || ''"
|
||||
:style="item.textStyle ?? ''"
|
||||
>{{ item.text }}</FormSwitch
|
||||
>
|
||||
</span>
|
||||
|
@ -136,7 +136,7 @@
|
|||
v-if="item.icon"
|
||||
:class="icon(`${item.icon} ph-fw`)"
|
||||
></i>
|
||||
<span :style="item.textStyle || ''">{{
|
||||
<span :style="item.textStyle ?? ''">{{
|
||||
item.text
|
||||
}}</span>
|
||||
<span class="caret"
|
||||
|
@ -166,7 +166,7 @@
|
|||
class="avatar"
|
||||
disable-link
|
||||
/>
|
||||
<span :style="item.textStyle || ''">{{
|
||||
<span :style="item.textStyle ?? ''">{{
|
||||
item.text
|
||||
}}</span>
|
||||
<span
|
||||
|
|
|
@ -48,7 +48,7 @@ const color = accent.toRgbString();
|
|||
|
||||
function draw(): void {
|
||||
const stats = props.src.slice().reverse();
|
||||
const peak = Math.max.apply(null, stats) || 1;
|
||||
const peak = Math.max(1, ...stats);
|
||||
|
||||
const _polylinePoints = stats.map((n, i) => [
|
||||
i * (viewBoxX / (stats.length - 1)),
|
||||
|
|
|
@ -225,7 +225,7 @@
|
|||
<XQuoteButton class="button" :note="appearNote" />
|
||||
<button
|
||||
v-if="
|
||||
$i != null &&
|
||||
isSignedIn &&
|
||||
isForeignLanguage &&
|
||||
translation == null
|
||||
"
|
||||
|
@ -294,7 +294,7 @@ import { userPage } from "@/filters/user";
|
|||
import * as os from "@/os";
|
||||
import { defaultStore, noteViewInterruptors } from "@/store";
|
||||
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import { i18n } from "@/i18n";
|
||||
import { getNoteMenu } from "@/scripts/get-note-menu";
|
||||
import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||
|
@ -353,7 +353,7 @@ const reactButton = ref<HTMLElement>();
|
|||
const appearNote = computed(() =>
|
||||
isRenote ? (note.value.renote as firefish.entities.Note) : note.value,
|
||||
);
|
||||
const isMyRenote = $i && $i.id === note.value.userId;
|
||||
const isMyRenote = isSignedIn && $i.id === note.value.userId;
|
||||
const showContent = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref(
|
||||
|
@ -375,7 +375,7 @@ const isForeignLanguage: boolean =
|
|||
defaultStore.state.detectPostLanguage &&
|
||||
appearNote.value.text != null &&
|
||||
(() => {
|
||||
const targetLang = (translateLang || lang || navigator.language)?.slice(
|
||||
const targetLang = (translateLang ?? lang ?? navigator.language)?.slice(
|
||||
0,
|
||||
2,
|
||||
);
|
||||
|
@ -395,7 +395,7 @@ async function translate() {
|
|||
translating.value = true;
|
||||
translation.value = await translate_(
|
||||
appearNote.value.id,
|
||||
translateLang || lang || navigator.language,
|
||||
translateLang ?? lang ?? navigator.language,
|
||||
);
|
||||
|
||||
// use UI language as the second translation language
|
||||
|
@ -403,7 +403,7 @@ async function translate() {
|
|||
translateLang != null &&
|
||||
lang != null &&
|
||||
translateLang !== lang &&
|
||||
(!translation.value ||
|
||||
(translation.value == null ||
|
||||
translation.value.sourceLang.toLowerCase() ===
|
||||
translateLang.slice(0, 2))
|
||||
)
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
<XQuoteButton class="button" :note="appearNote" />
|
||||
<button
|
||||
v-if="
|
||||
$i != null &&
|
||||
isSignedIn &&
|
||||
isForeignLanguage &&
|
||||
translation == null
|
||||
"
|
||||
|
@ -210,7 +210,7 @@ import { useRouter } from "@/router";
|
|||
import { userPage } from "@/filters/user";
|
||||
import * as os from "@/os";
|
||||
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import { i18n } from "@/i18n";
|
||||
import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||
import { defaultStore } from "@/store";
|
||||
|
@ -292,7 +292,7 @@ const isForeignLanguage: boolean =
|
|||
defaultStore.state.detectPostLanguage &&
|
||||
appearNote.value.text != null &&
|
||||
(() => {
|
||||
const targetLang = (translateLang || lang || navigator.language)?.slice(
|
||||
const targetLang = (translateLang ?? lang ?? navigator.language)?.slice(
|
||||
0,
|
||||
2,
|
||||
);
|
||||
|
@ -312,7 +312,7 @@ async function translate() {
|
|||
translating.value = true;
|
||||
translation.value = await translate_(
|
||||
appearNote.value.id,
|
||||
translateLang || lang || navigator.language,
|
||||
translateLang ?? lang ?? navigator.language,
|
||||
);
|
||||
|
||||
// use UI language as the second translation language
|
||||
|
@ -320,7 +320,7 @@ async function translate() {
|
|||
translateLang != null &&
|
||||
lang != null &&
|
||||
translateLang !== lang &&
|
||||
(!translation.value ||
|
||||
(translation.value == null ||
|
||||
translation.value.sourceLang.toLowerCase() ===
|
||||
translateLang.slice(0, 2))
|
||||
)
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
class="notes"
|
||||
>
|
||||
<XNote
|
||||
:key="note._featuredId_ || note._prId_ || note.id"
|
||||
:key="note._featuredId_ ?? note._prId_ ?? note.id"
|
||||
class="qtqtichx"
|
||||
:note="note"
|
||||
/>
|
||||
|
|
|
@ -64,7 +64,7 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
const includingTypes = computed(() => props.includingTypes || []);
|
||||
const includingTypes = computed(() => props.includingTypes ?? []);
|
||||
|
||||
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
||||
|
||||
|
|
|
@ -181,7 +181,7 @@ onMounted(() => {
|
|||
}
|
||||
for (
|
||||
let i = 0;
|
||||
i < (pagingComponent.value.items || []).length;
|
||||
i < (pagingComponent.value.items ?? []).length;
|
||||
i++
|
||||
) {
|
||||
if (
|
||||
|
|
|
@ -18,7 +18,7 @@ const tweened = reactive({
|
|||
watch(
|
||||
() => props.value,
|
||||
(n) => {
|
||||
gsap.to(tweened, { duration: 0.6, number: Number(n) || 0 });
|
||||
gsap.to(tweened, { duration: 0.6, number: Number(n) ?? 0 });
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
|
|
|
@ -151,8 +151,8 @@ const init = async (): Promise<void> => {
|
|||
.api(props.pagination.endpoint, {
|
||||
...params,
|
||||
limit: props.pagination.noPaging
|
||||
? props.pagination.limit || 10
|
||||
: (props.pagination.limit || 10) + 1,
|
||||
? props.pagination.limit ?? 10
|
||||
: (props.pagination.limit ?? 10) + 1,
|
||||
})
|
||||
.then(
|
||||
(res) => {
|
||||
|
|
|
@ -389,7 +389,7 @@ const draghover = ref(false);
|
|||
const quoteId = ref(null);
|
||||
const hasNotSpecifiedMentions = ref(false);
|
||||
const recentHashtags = ref(
|
||||
JSON.parse(localStorage.getItem("hashtags") || "[]"),
|
||||
JSON.parse(localStorage.getItem("hashtags") ?? "[]"),
|
||||
);
|
||||
const imeText = ref("");
|
||||
|
||||
|
@ -460,10 +460,10 @@ const canPost = computed((): boolean => {
|
|||
!posting.value &&
|
||||
(textLength.value >= 1 ||
|
||||
files.value.length >= 1 ||
|
||||
!!poll.value ||
|
||||
!!props.renote) &&
|
||||
poll.value != null ||
|
||||
props.renote != null) &&
|
||||
textLength.value <= maxTextLength.value &&
|
||||
(!poll.value || poll.value.choices.length >= 2)
|
||||
(poll.value == null || poll.value.choices.length >= 2)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -970,7 +970,7 @@ function onDrop(ev): void {
|
|||
}
|
||||
|
||||
function saveDraft() {
|
||||
const draftData = JSON.parse(localStorage.getItem("drafts") || "{}");
|
||||
const draftData = JSON.parse(localStorage.getItem("drafts") ?? "{}");
|
||||
|
||||
draftData[draftKey.value] = {
|
||||
updatedAt: new Date(),
|
||||
|
@ -990,7 +990,7 @@ function saveDraft() {
|
|||
}
|
||||
|
||||
function deleteDraft() {
|
||||
const draftData = JSON.parse(localStorage.getItem("drafts") || "{}");
|
||||
const draftData = JSON.parse(localStorage.getItem("drafts") ?? "{}");
|
||||
|
||||
delete draftData[draftKey.value];
|
||||
|
||||
|
@ -1013,7 +1013,7 @@ async function post() {
|
|||
: undefined,
|
||||
channelId: props.channel ? props.channel.id : undefined,
|
||||
poll: poll.value,
|
||||
cw: useCw.value ? cw.value || "" : undefined,
|
||||
cw: useCw.value ? cw.value ?? "" : undefined,
|
||||
lang: language.value ? language.value : undefined,
|
||||
localOnly: localOnly.value,
|
||||
visibility:
|
||||
|
@ -1065,7 +1065,7 @@ async function post() {
|
|||
.filter((x) => x.type === "hashtag")
|
||||
.map((x) => x.props.hashtag);
|
||||
const history = JSON.parse(
|
||||
localStorage.getItem("hashtags") || "[]",
|
||||
localStorage.getItem("hashtags") ?? "[]",
|
||||
) as string[];
|
||||
localStorage.setItem(
|
||||
"hashtags",
|
||||
|
@ -1074,6 +1074,7 @@ async function post() {
|
|||
}
|
||||
posting.value = false;
|
||||
postAccount.value = null;
|
||||
nextTick(() => autosize.update(textareaEl.value));
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -1183,7 +1184,7 @@ onMounted(() => {
|
|||
autosize(textareaEl.value);
|
||||
// 書きかけの投稿を復元
|
||||
if (!props.instant && !props.mention && !props.specified) {
|
||||
const draft = JSON.parse(localStorage.getItem("drafts") || "{}")[
|
||||
const draft = JSON.parse(localStorage.getItem("drafts") ?? "{}")[
|
||||
draftKey.value
|
||||
];
|
||||
if (draft) {
|
||||
|
@ -1193,7 +1194,7 @@ onMounted(() => {
|
|||
visibility.value = draft.data.visibility;
|
||||
localOnly.value = draft.data.localOnly;
|
||||
language.value = draft.data.lang;
|
||||
files.value = (draft.data.files || []).filter(
|
||||
files.value = (draft.data.files ?? []).filter(
|
||||
(draftFile) => draftFile,
|
||||
);
|
||||
if (draft.data.poll) {
|
||||
|
@ -1223,6 +1224,7 @@ onMounted(() => {
|
|||
quoteId.value = init.renote ? init.renote.id : null;
|
||||
}
|
||||
|
||||
nextTick(() => autosize.update(textareaEl.value));
|
||||
nextTick(() => watchForDraft());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -105,7 +105,7 @@ async function describe(file) {
|
|||
},
|
||||
{
|
||||
done: (result) => {
|
||||
if (!result || result.canceled) return;
|
||||
if (result == null || result.canceled) return;
|
||||
const comment =
|
||||
result.result.length === 0 ? null : result.result;
|
||||
os.api("drive/files/update", {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<MkButton
|
||||
v-else-if="
|
||||
!showOnlyToRegister &&
|
||||
($i ? pushRegistrationInServer : pushSubscription)
|
||||
(isSignedIn ? pushRegistrationInServer : pushSubscription)
|
||||
"
|
||||
type="button"
|
||||
:primary="false"
|
||||
|
@ -31,7 +31,7 @@
|
|||
{{ i18n.ts.unsubscribePushNotification }}
|
||||
</MkButton>
|
||||
<MkButton
|
||||
v-else-if="$i && pushRegistrationInServer"
|
||||
v-else-if="isSignedIn && pushRegistrationInServer"
|
||||
disabled
|
||||
:rounded="rounded"
|
||||
:inline="inline"
|
||||
|
@ -56,7 +56,7 @@
|
|||
import { ref } from "vue";
|
||||
|
||||
import { getAccounts } from "@/account";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import { instance } from "@/instance";
|
||||
import { api, apiWithDialog, promiseDialog } from "@/os";
|
||||
|
@ -147,7 +147,7 @@ async function unsubscribe() {
|
|||
|
||||
pushRegistrationInServer.value = undefined;
|
||||
|
||||
if ($i && accounts.length >= 2) {
|
||||
if (isSignedIn && accounts.length >= 2) {
|
||||
apiWithDialog("sw/unregister", {
|
||||
i: $i.token,
|
||||
endpoint,
|
||||
|
@ -193,7 +193,12 @@ if (navigator.serviceWorker == null) {
|
|||
pushSubscription.value =
|
||||
await registration.value.pushManager.getSubscription();
|
||||
|
||||
if (instance.swPublickey && "PushManager" in window && $i && $i.token) {
|
||||
if (
|
||||
instance.swPublickey &&
|
||||
"PushManager" in window &&
|
||||
isSignedIn &&
|
||||
$i.token
|
||||
) {
|
||||
supported.value = true;
|
||||
|
||||
if (pushSubscription.value) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkEmoji
|
||||
:emoji="reaction"
|
||||
:custom-emojis="customEmojis || []"
|
||||
:custom-emojis="customEmojis ?? []"
|
||||
:is-reaction="true"
|
||||
:normal="true"
|
||||
:no-style="noStyle"
|
||||
|
|
|
@ -28,7 +28,7 @@ import XDetails from "@/components/MkReactionsViewer.details.vue";
|
|||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||
import * as os from "@/os";
|
||||
import { useTooltip } from "@/scripts/use-tooltip";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { isSignedIn } from "@/reactiveAccount";
|
||||
|
||||
const props = defineProps<{
|
||||
reaction: string;
|
||||
|
@ -43,7 +43,7 @@ const emit = defineEmits<{
|
|||
|
||||
const buttonRef = ref<HTMLElement>();
|
||||
|
||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
const canToggle = computed(() => isSignedIn && !props.reaction.match(/@\w/));
|
||||
|
||||
const toggleReaction = () => {
|
||||
if (!canToggle.value) return;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import type * as firefish from "firefish-js";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import XReaction from "@/components/MkReactionsViewer.reaction.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -30,7 +30,7 @@ const reactionsEl = ref<HTMLElement>();
|
|||
|
||||
const initialReactions = new Set(Object.keys(props.note.reactions));
|
||||
|
||||
const isMe = computed(() => $i && $i.id === props.note.userId);
|
||||
const isMe = computed(() => isSignedIn && $i.id === props.note.userId);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
78
packages/client/src/components/MkReleaseNotesWindow.vue
Normal file
78
packages/client/src/components/MkReleaseNotesWindow.vue
Normal file
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<XWindow
|
||||
:initial-width="800"
|
||||
:can-resize="true"
|
||||
:front="true"
|
||||
:buttons-right="buttonsRight"
|
||||
@closed="emit('closed')"
|
||||
class="oxzftdfc"
|
||||
>
|
||||
<template #header>
|
||||
{{ i18n.ts.releaseNotes }}
|
||||
</template>
|
||||
<div v-if="!translating && translations.length === 0" class="asnohbod">
|
||||
<ul>
|
||||
<li v-for="(item, i) in notes" :key="i">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<MkLoading v-else-if="translating" mini />
|
||||
<div v-else class="asnohbod">
|
||||
<ul>
|
||||
<li v-for="(item, i) in translations" :key="i">
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</XWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import XWindow from "@/components/MkWindow.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
import { api } from "@/os";
|
||||
import icon from "@/scripts/icon";
|
||||
|
||||
defineProps<{
|
||||
notes: string[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: "closed"): void;
|
||||
}>();
|
||||
|
||||
const translating = ref(false);
|
||||
const translations = ref([] as string[]);
|
||||
|
||||
const lang = localStorage.getItem("lang");
|
||||
const translateLang = localStorage.getItem("translateLang");
|
||||
|
||||
async function translate() {
|
||||
translating.value = true;
|
||||
translations.value = await api("release/translate", {
|
||||
targetLang: translateLang ?? lang ?? navigator.language,
|
||||
});
|
||||
translating.value = false;
|
||||
}
|
||||
|
||||
const buttonsRight = computed(() => [
|
||||
{
|
||||
icon: `${icon("ph-translate")}`,
|
||||
title: i18n.ts.translate,
|
||||
onClick: translate,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.oxzftdfc {
|
||||
max-height: 70%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.asnohbod {
|
||||
white-space: pre-wrap;
|
||||
font-size: 1.2em;
|
||||
padding: 5px 20px 10px;
|
||||
}
|
||||
</style>
|
|
@ -27,7 +27,7 @@ import Ripple from "@/components/MkRipple.vue";
|
|||
import XDetails from "@/components/MkUsersTooltip.vue";
|
||||
import { pleaseLogin } from "@/scripts/please-login";
|
||||
import * as os from "@/os";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import { useTooltip } from "@/scripts/use-tooltip";
|
||||
import { i18n } from "@/i18n";
|
||||
import { defaultStore } from "@/store";
|
||||
|
@ -74,7 +74,7 @@ useTooltip(buttonRef, async (showing) => {
|
|||
|
||||
const hasRenotedBefore = ref(false);
|
||||
|
||||
if ($i != null) {
|
||||
if (isSignedIn) {
|
||||
os.api("notes/renotes", {
|
||||
noteId: props.note.id,
|
||||
userId: $i.id,
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<template #suffix>@{{ host }}</template>
|
||||
</MkInput>
|
||||
<MkInput
|
||||
v-if="!user || (user && !user.usePasswordLessLogin)"
|
||||
v-if="user == null || (user && !user.usePasswordLessLogin)"
|
||||
v-model="password"
|
||||
class="_formBlock"
|
||||
:placeholder="i18n.ts.password"
|
||||
|
|
|
@ -310,12 +310,12 @@ const host = toUnicode(config.host);
|
|||
const hcaptcha = ref();
|
||||
const recaptcha = ref();
|
||||
|
||||
const username: string = ref("");
|
||||
const password: string = ref("");
|
||||
const retypedPassword: string = ref("");
|
||||
const invitationCode: string = ref("");
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const retypedPassword = ref("");
|
||||
const invitationCode = ref("");
|
||||
const email = ref("");
|
||||
const usernameState:
|
||||
const usernameState = ref<
|
||||
| null
|
||||
| "wait"
|
||||
| "ok"
|
||||
|
@ -323,9 +323,10 @@ const usernameState:
|
|||
| "error"
|
||||
| "invalid-format"
|
||||
| "min-range"
|
||||
| "max-range" = ref(null);
|
||||
const invitationState: null | "entered" = ref(null);
|
||||
const emailState:
|
||||
| "max-range"
|
||||
>(null);
|
||||
const invitationState = ref<null | "entered">(null);
|
||||
const emailState = ref<
|
||||
| null
|
||||
| "wait"
|
||||
| "ok"
|
||||
|
@ -335,11 +336,12 @@ const emailState:
|
|||
| "unavailable:mx"
|
||||
| "unavailable:smtp"
|
||||
| "unavailable"
|
||||
| "error" = ref(null);
|
||||
const passwordStrength: "" | "low" | "medium" | "high" = ref("");
|
||||
const passwordRetypeState: null | "match" | "not-match" = ref(null);
|
||||
const submitting: boolean = ref(false);
|
||||
const ToSAgreement: boolean = ref(false);
|
||||
| "error"
|
||||
>(null);
|
||||
const passwordStrength = ref<"" | "low" | "medium" | "high">("");
|
||||
const passwordRetypeState = ref<null | "match" | "not-match">(null);
|
||||
const submitting = ref(false);
|
||||
const ToSAgreement = ref(false);
|
||||
const hCaptchaResponse = ref(null);
|
||||
const reCaptchaResponse = ref(null);
|
||||
|
||||
|
|
|
@ -341,6 +341,7 @@ function focusFooter(ev) {
|
|||
|
||||
&.collapsed,
|
||||
&.showContent {
|
||||
display: block;
|
||||
position: relative;
|
||||
max-height: calc(15em + 100px);
|
||||
> .body {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkInfo
|
||||
v-if="tlHint && !tlHintClosed"
|
||||
v-if="tlHint && !tlHintClosed && isSignedIn"
|
||||
:closeable="true"
|
||||
class="_gap"
|
||||
@close="closeHint"
|
||||
|
@ -49,7 +49,7 @@ import MkPullToRefresh from "@/components/MkPullToRefresh.vue";
|
|||
import MkInfo from "@/components/MkInfo.vue";
|
||||
import { useStream } from "@/stream";
|
||||
import * as sound from "@/scripts/sound";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import { i18n } from "@/i18n";
|
||||
import { defaultStore } from "@/store";
|
||||
import icon from "@/scripts/icon";
|
||||
|
@ -82,7 +82,7 @@ const prepend = (note) => {
|
|||
tlComponent.value?.pagingComponent?.prepend(note);
|
||||
emit("note");
|
||||
if (props.sound) {
|
||||
sound.play($i && note.userId === $i.id ? "noteMy" : "note");
|
||||
sound.play(isSignedIn && note.userId === $i.id ? "noteMy" : "note");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
@closed="$emit('closed')"
|
||||
@ok="ok()"
|
||||
>
|
||||
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
|
||||
<template #header>{{ title ?? i18n.ts.generateAccessToken }}</template>
|
||||
<div v-if="information" class="_section">
|
||||
<MkInfo warn>{{ information }}</MkInfo>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@
|
|||
i18n.ts.enableAll
|
||||
}}</MkButton>
|
||||
<MkSwitch
|
||||
v-for="kind in initialPermissions || kinds"
|
||||
v-for="kind in initialPermissions ?? kinds"
|
||||
:key="kind"
|
||||
v-model="permissions[kind]"
|
||||
style="margin-bottom: 6px"
|
||||
|
|
|
@ -208,19 +208,15 @@ import MkPushNotificationAllowButton from "@/components/MkPushNotificationAllowB
|
|||
import FormSwitch from "@/components/form/switch.vue";
|
||||
import { defaultStore } from "@/store";
|
||||
import { i18n } from "@/i18n";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { isModerator } from "@/reactiveAccount";
|
||||
import { instance } from "@/instance";
|
||||
import icon from "@/scripts/icon";
|
||||
|
||||
const isLocalTimelineAvailable =
|
||||
!instance.disableLocalTimeline ||
|
||||
($i != null && ($i.isModerator || $i.isAdmin));
|
||||
const isLocalTimelineAvailable = !instance.disableLocalTimeline || isModerator;
|
||||
const isRecommendedTimelineAvailable =
|
||||
!instance.disableRecommendedTimeline ||
|
||||
($i != null && ($i.isModerator || $i.isAdmin));
|
||||
!instance.disableRecommendedTimeline || isModerator;
|
||||
const isGlobalTimelineAvailable =
|
||||
!instance.disableGlobalTimeline ||
|
||||
($i != null && ($i.isModerator || $i.isAdmin));
|
||||
!instance.disableGlobalTimeline || isModerator;
|
||||
|
||||
const timelines = ["home"];
|
||||
|
||||
|
@ -246,7 +242,7 @@ const dialog = ref<InstanceType<typeof XModalWindow>>();
|
|||
|
||||
const tutorial = computed({
|
||||
get() {
|
||||
return defaultStore.reactiveState.tutorial.value || 0;
|
||||
return defaultStore.reactiveState.tutorial.value ?? 0;
|
||||
},
|
||||
set(value) {
|
||||
defaultStore.set("tutorial", value);
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle>
|
||||
</div>
|
||||
<div :class="$style.version">✨ {{ version }} 🚀</div>
|
||||
<div v-if="newRelease" :class="$style.releaseNotes">
|
||||
<!-- <div :class="$style.releaseNotes">
|
||||
<Mfm :text="data.notes" />
|
||||
<div v-if="data.screenshots.length > 0" style="max-width: 500">
|
||||
<img
|
||||
|
@ -20,7 +20,14 @@
|
|||
alt="screenshot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<MkButton
|
||||
v-if="notes.length > 0"
|
||||
:class="$style.gotIt"
|
||||
full
|
||||
@click="openReleaseNotes"
|
||||
>{{ i18n.ts.whatIsNew }}</MkButton
|
||||
>
|
||||
<MkButton
|
||||
:class="$style.gotIt"
|
||||
primary
|
||||
|
@ -33,7 +40,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from "vue";
|
||||
import { defineAsyncComponent, ref, shallowRef } from "vue";
|
||||
import MkModal from "@/components/MkModal.vue";
|
||||
import MkSparkle from "@/components/MkSparkle.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
|
@ -43,18 +50,24 @@ import * as os from "@/os";
|
|||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
const newRelease = ref(false);
|
||||
const data = ref(Object);
|
||||
const notes = ref([] as string[]);
|
||||
|
||||
os.api("release").then((res) => {
|
||||
data.value = res;
|
||||
newRelease.value = version === data.value?.version;
|
||||
notes.value = res.notes.trim().split("\n");
|
||||
});
|
||||
|
||||
console.log(`Version: ${version}`);
|
||||
console.log(`Data version: ${data.value.version}`);
|
||||
console.log(newRelease.value);
|
||||
console.log(data.value);
|
||||
function openReleaseNotes(): void {
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkReleaseNotesWindow.vue"),
|
||||
),
|
||||
{
|
||||
notes: notes.value,
|
||||
},
|
||||
{},
|
||||
"closed",
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
:target="target"
|
||||
:title="url"
|
||||
:class="{
|
||||
hasButton: tweetId || player.url,
|
||||
hasButton: tweetId ?? player.url,
|
||||
}"
|
||||
>
|
||||
<div v-if="thumbnail" class="thumbnail">
|
||||
|
@ -51,10 +51,10 @@
|
|||
<MkLoading mini />
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3 :title="title || undefined">{{ title || url }}</h3>
|
||||
<p :title="description">
|
||||
<h3 :title="title ?? undefined">{{ title ?? url }}</h3>
|
||||
<p :title="description ?? undefined">
|
||||
<span>
|
||||
<span :title="sitename || undefined">
|
||||
<span :title="sitename ?? undefined">
|
||||
<img v-if="icon" class="icon" :src="icon" />
|
||||
{{ sitename }}
|
||||
</span>
|
||||
|
@ -72,7 +72,7 @@
|
|||
: '?autoplay=1&auto_play=1')
|
||||
"
|
||||
:style="`aspect-ratio: ${
|
||||
(player.width || 1) / (player.height || 1)
|
||||
(player.width ?? 1) / (player.height ?? 1)
|
||||
}`"
|
||||
frameborder="0"
|
||||
allow="autoplay; encrypted-media"
|
||||
|
@ -153,7 +153,7 @@ if (
|
|||
requestUrl.hostname = "www.youtube.com";
|
||||
}
|
||||
|
||||
const requestLang = (lang || "ja-JP").replace("ja-KS", "ja-JP");
|
||||
const requestLang = (lang ?? "ja-JP").replace("ja-KS", "ja-JP");
|
||||
|
||||
requestUrl.hash = "";
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
:class="{ detailed }"
|
||||
>
|
||||
<span
|
||||
v-if="$i && $i.id != user.id && user.isFollowed"
|
||||
v-if="isSignedIn && $i.id != user.id && user.isFollowed"
|
||||
class="followed"
|
||||
:class="{ 'followed-emph': emphasizeFollowed }"
|
||||
>{{ i18n.ts.followsYou }}</span
|
||||
|
@ -80,7 +80,10 @@
|
|||
</div>
|
||||
<div class="buttons">
|
||||
<slot>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" :user="user" />
|
||||
<MkFollowButton
|
||||
v-if="isSignedIn && user.id !== $i.id"
|
||||
:user="user"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</article>
|
||||
|
@ -96,6 +99,7 @@ import MkNumber from "@/components/MkNumber.vue";
|
|||
import { userPage } from "@/filters/user";
|
||||
import { i18n } from "@/i18n";
|
||||
import { defaultStore } from "@/store";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
|
||||
const props = defineProps<{
|
||||
user: firefish.entities.UserDetailed;
|
||||
|
|
|
@ -549,7 +549,8 @@ defineExpose({
|
|||
}
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<span
|
||||
v-if="user.host || detail || defaultStore.state.showFullAcct"
|
||||
class="host"
|
||||
>@{{ user.host || host }}</span
|
||||
>@{{ user.host ?? host }}</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<Mfm
|
||||
v-if="show"
|
||||
:class="$style.root"
|
||||
:text="user.name || user.username"
|
||||
:text="user.name ?? user.username"
|
||||
:plain="true"
|
||||
:nowrap="nowrap"
|
||||
:custom-emojis="user.emojis"
|
||||
|
|
|
@ -113,31 +113,31 @@ export default defineComponent({
|
|||
let style: string;
|
||||
switch (token.props.name) {
|
||||
case "tada": {
|
||||
const speed = validTime(token.props.args.speed) || "1s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
// const ease = validEase(token.props.args.ease) || "linear";
|
||||
const speed = validTime(token.props.args.speed) ?? "1s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
// const ease = validEase(token.props.args.ease) ?? "linear";
|
||||
style = `font-size: 150%; animation: tada ${speed} ${delay} linear ${loop} both;`;
|
||||
break;
|
||||
}
|
||||
case "jelly": {
|
||||
const speed = validTime(token.props.args.speed) || "1s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
const speed = validTime(token.props.args.speed) ?? "1s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
style = `animation: mfm-rubberBand ${speed} ${delay} linear ${loop} both;`;
|
||||
break;
|
||||
}
|
||||
case "twitch": {
|
||||
const speed = validTime(token.props.args.speed) || "0.5s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
const speed = validTime(token.props.args.speed) ?? "0.5s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
style = `animation: mfm-twitch ${speed} ${delay} ease ${loop};`;
|
||||
break;
|
||||
}
|
||||
case "shake": {
|
||||
const speed = validTime(token.props.args.speed) || "0.5s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
const speed = validTime(token.props.args.speed) ?? "0.5s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
style = `animation: mfm-shake ${speed} ${delay} ease ${loop};`;
|
||||
break;
|
||||
}
|
||||
|
@ -152,30 +152,30 @@ export default defineComponent({
|
|||
: token.props.args.y
|
||||
? "mfm-spinY"
|
||||
: "mfm-spin";
|
||||
const speed = validTime(token.props.args.speed) || "1.5s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
const speed = validTime(token.props.args.speed) ?? "1.5s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
style = `animation: ${anime} ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`;
|
||||
break;
|
||||
}
|
||||
case "jump": {
|
||||
const speed = validTime(token.props.args.speed) || "0.75s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
const speed = validTime(token.props.args.speed) ?? "0.75s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
style = `animation: mfm-jump ${speed} ${delay} linear ${loop};`;
|
||||
break;
|
||||
}
|
||||
case "bounce": {
|
||||
const speed = validTime(token.props.args.speed) || "0.75s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
const speed = validTime(token.props.args.speed) ?? "0.75s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
style = `animation: mfm-bounce ${speed} ${delay} linear ${loop}; transform-origin: center bottom;`;
|
||||
break;
|
||||
}
|
||||
case "rainbow": {
|
||||
const speed = validTime(token.props.args.speed) || "1s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
const speed = validTime(token.props.args.speed) ?? "1s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
style = `animation: mfm-rainbow ${speed} ${delay} linear ${loop};`;
|
||||
break;
|
||||
}
|
||||
|
@ -189,9 +189,9 @@ export default defineComponent({
|
|||
const direction = token.props.args.out
|
||||
? "alternate-reverse"
|
||||
: "alternate";
|
||||
const speed = validTime(token.props.args.speed) || "1.5s";
|
||||
const delay = validTime(token.props.args.delay) || "0s";
|
||||
const loop = validNumber(token.props.args.loop) || "infinite";
|
||||
const speed = validTime(token.props.args.speed) ?? "1.5s";
|
||||
const delay = validTime(token.props.args.delay) ?? "0s";
|
||||
const loop = validNumber(token.props.args.loop) ?? "infinite";
|
||||
style = `animation: mfm-fade ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`;
|
||||
break;
|
||||
}
|
||||
|
@ -395,7 +395,7 @@ export default defineComponent({
|
|||
this.author &&
|
||||
this.author.host != null
|
||||
? this.author.host
|
||||
: token.props.host) || host,
|
||||
: token.props.host) ?? host,
|
||||
username: token.props.username,
|
||||
}),
|
||||
];
|
||||
|
|
|
@ -35,7 +35,7 @@ export default defineComponent({
|
|||
function click() {
|
||||
props.hpml.updatePageVar(
|
||||
props.block.name,
|
||||
value.value + (props.block.inc || 1),
|
||||
value.value + (props.block.inc ?? 1),
|
||||
);
|
||||
props.hpml.eval();
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ function calc(el: Element) {
|
|||
const info = mountings.get(el);
|
||||
const width = el.clientWidth;
|
||||
|
||||
if (!info || info.previousWidth === width) return;
|
||||
if (info == null || info.previousWidth === width) return;
|
||||
|
||||
// アクティベート前などでsrcが描画されていない場合
|
||||
if (!width) {
|
||||
|
|
|
@ -7,7 +7,7 @@ export const acct = (user: firefish.Acct) => {
|
|||
};
|
||||
|
||||
export const userName = (user: firefish.entities.User) => {
|
||||
return user.name || user.username;
|
||||
return user.name ?? user.username;
|
||||
};
|
||||
|
||||
export const userPage = (user: firefish.Acct, path?, absolute = false) => {
|
||||
|
|
|
@ -22,7 +22,7 @@ if (accounts) {
|
|||
}
|
||||
// #endregion
|
||||
|
||||
import { compareVersions } from "compare-versions";
|
||||
// import { compareVersions } from "compare-versions";
|
||||
import {
|
||||
computed,
|
||||
createApp,
|
||||
|
@ -41,7 +41,7 @@ import directives from "@/directives";
|
|||
import { i18n } from "@/i18n";
|
||||
import { fetchInstance, instance } from "@/instance";
|
||||
import { alert, api, confirm, popup, post, toast } from "@/os";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import { deviceKind } from "@/scripts/device-kind";
|
||||
import { getAccountFromId } from "@/scripts/get-account-from-id";
|
||||
import { makeHotkey } from "@/scripts/hotkey";
|
||||
|
@ -123,7 +123,7 @@ function checkForSplash() {
|
|||
|
||||
// #region Set lang attr
|
||||
const html = document.documentElement;
|
||||
html.setAttribute("lang", lang || "en-US");
|
||||
html.setAttribute("lang", lang ?? "en-US");
|
||||
html.setAttribute("dir", langmap[lang].rtl === true ? "rtl" : "ltr");
|
||||
//#endregion
|
||||
|
||||
|
@ -134,7 +134,7 @@ function checkForSplash() {
|
|||
if (loginId) {
|
||||
const target = getUrlWithoutLoginId(location.href);
|
||||
|
||||
if (!$i || $i.id !== loginId) {
|
||||
if ($i == null || $i.id !== loginId) {
|
||||
const account = await getAccountFromId(loginId);
|
||||
if (account) {
|
||||
await login(account.token, target);
|
||||
|
@ -253,7 +253,7 @@ function checkForSplash() {
|
|||
// テーマリビルドするため
|
||||
localStorage.removeItem("theme");
|
||||
|
||||
if ($i && defaultStore.state.showUpdates) {
|
||||
if (isSignedIn && defaultStore.state.showUpdates) {
|
||||
popup(
|
||||
defineAsyncComponent(() => import("@/components/MkUpdated.vue")),
|
||||
{},
|
||||
|
@ -264,7 +264,7 @@ function checkForSplash() {
|
|||
}
|
||||
|
||||
if (
|
||||
$i &&
|
||||
isSignedIn &&
|
||||
defaultStore.state.tutorial === -1 &&
|
||||
!["/announcements", "/announcements/"].includes(window.location.pathname)
|
||||
) {
|
||||
|
@ -418,7 +418,7 @@ function checkForSplash() {
|
|||
s: search,
|
||||
};
|
||||
|
||||
if ($i) {
|
||||
if (isSignedIn) {
|
||||
// only add post shortcuts if logged in
|
||||
hotkeys["p|n"] = post;
|
||||
|
||||
|
@ -437,7 +437,7 @@ function checkForSplash() {
|
|||
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
|
||||
toast(
|
||||
i18n.t("welcomeBackWithName", {
|
||||
name: $i.name || $i.username,
|
||||
name: $i.name ?? $i.username,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { computed, reactive } from "vue";
|
|||
import { ui } from "@/config";
|
||||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
import { $i } from "@/reactiveAccount";
|
||||
import { $i, isSignedIn } from "@/reactiveAccount";
|
||||
import icon from "@/scripts/icon";
|
||||
import { search } from "@/scripts/search";
|
||||
import { unisonReload } from "@/scripts/unison-reload";
|
||||
|
@ -11,21 +11,21 @@ export const navbarItemDef = reactive({
|
|||
notifications: {
|
||||
title: "notifications",
|
||||
icon: `${icon("ph-bell")}`,
|
||||
show: computed(() => $i != null),
|
||||
show: computed(() => isSignedIn),
|
||||
indicated: computed(() => $i?.hasUnreadNotification),
|
||||
to: "/my/notifications",
|
||||
},
|
||||
messaging: {
|
||||
title: "messaging",
|
||||
icon: `${icon("ph-chats-teardrop")}`,
|
||||
show: computed(() => $i != null),
|
||||
show: computed(() => isSignedIn),
|
||||
indicated: computed(() => $i?.hasUnreadMessagingMessage),
|
||||
to: "/my/messaging",
|
||||
},
|
||||
drive: {
|
||||
title: "drive",
|
||||
icon: `${icon("ph-cloud")}`,
|
||||
show: computed(() => $i != null),
|
||||
show: computed(() => isSignedIn),
|
||||
to: "/my/drive",
|
||||
},
|
||||
followRequests: {
|
||||
|
@ -54,19 +54,19 @@ export const navbarItemDef = reactive({
|
|||
lists: {
|
||||
title: "lists",
|
||||
icon: `${icon("ph-list-bullets")}`,
|
||||
show: computed(() => $i != null),
|
||||
show: computed(() => isSignedIn),
|
||||
to: "/my/lists",
|
||||
},
|
||||
antennas: {
|
||||
title: "antennas",
|
||||
icon: `${icon("ph-flying-saucer")}`,
|
||||
show: computed(() => $i != null),
|
||||
show: computed(() => isSignedIn),
|
||||
to: "/my/antennas",
|
||||
},
|
||||
favorites: {
|
||||
title: "favorites",
|
||||
icon: `${icon("ph-bookmark-simple")}`,
|
||||
show: computed(() => $i != null),
|
||||
show: computed(() => isSignedIn),
|
||||
to: "/my/favorites",
|
||||
},
|
||||
pages: {
|
||||
|
@ -82,7 +82,7 @@ export const navbarItemDef = reactive({
|
|||
clips: {
|
||||
title: "clips",
|
||||
icon: `${icon("ph-paperclip")}`,
|
||||
show: computed(() => $i != null),
|
||||
show: computed(() => isSignedIn),
|
||||
to: "/my/clips",
|
||||
},
|
||||
channels: {
|
||||
|
|
|
@ -314,7 +314,7 @@ export function confirm(props: {
|
|||
},
|
||||
{
|
||||
done: (result) => {
|
||||
resolve(result || { canceled: true });
|
||||
resolve(result ?? { canceled: true });
|
||||
},
|
||||
},
|
||||
"closed",
|
||||
|
@ -342,7 +342,7 @@ export function yesno(props: {
|
|||
},
|
||||
{
|
||||
done: (result) => {
|
||||
resolve(result || { canceled: true });
|
||||
resolve(result ?? { canceled: true });
|
||||
},
|
||||
},
|
||||
"closed",
|
||||
|
@ -383,7 +383,7 @@ export function inputText(props: {
|
|||
},
|
||||
{
|
||||
done: (result) => {
|
||||
resolve(result || { canceled: true });
|
||||
resolve(result ?? { canceled: true });
|
||||
},
|
||||
},
|
||||
"closed",
|
||||
|
@ -421,7 +421,7 @@ export function inputParagraph(props: {
|
|||
},
|
||||
{
|
||||
done: (result) => {
|
||||
resolve(result || { canceled: true });
|
||||
resolve(result ?? { canceled: true });
|
||||
},
|
||||
},
|
||||
"closed",
|
||||
|
@ -461,7 +461,7 @@ export function inputNumber(props: {
|
|||
},
|
||||
{
|
||||
done: (result) => {
|
||||
resolve(result || { canceled: true });
|
||||
resolve(result ?? { canceled: true });
|
||||
},
|
||||
},
|
||||
"closed",
|
||||
|
@ -553,7 +553,7 @@ export function select<C = any>(
|
|||
},
|
||||
{
|
||||
done: (result) => {
|
||||
resolve(result || { canceled: true });
|
||||
resolve(result ?? { canceled: true });
|
||||
},
|
||||
},
|
||||
"closed",
|
||||
|
|
|
@ -216,7 +216,7 @@ import MkLink from "@/components/MkLink.vue";
|
|||
import MkSparkle from "@/components/MkSparkle.vue";
|
||||
import { physics } from "@/scripts/physics";
|
||||
import { i18n } from "@/i18n";
|
||||
import { defaultStore } from "@/store";
|
||||
import { defaultStore, defaultReactions } from "@/store";
|
||||
import * as os from "@/os";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import icon from "@/scripts/icon";
|
||||
|
@ -235,7 +235,10 @@ const easterEggEngine = ref(null);
|
|||
const containerEl = ref<HTMLElement>();
|
||||
|
||||
function iconLoaded() {
|
||||
const emojis = defaultStore.state.reactions;
|
||||
const emojis =
|
||||
defaultStore.state.reactions.length > 0
|
||||
? defaultStore.state.reactions
|
||||
: defaultReactions;
|
||||
const containerWidth = containerEl.value?.offsetWidth;
|
||||
for (let i = 0; i < 32; i++) {
|
||||
easterEggEmojis.value.push({
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
:key="category"
|
||||
class="emojis"
|
||||
>
|
||||
<template #header>{{ category || i18n.ts.other }}</template>
|
||||
<template #header>{{ category ?? i18n.ts.other }}</template>
|
||||
<div class="zuvgdzyt">
|
||||
<XEmoji
|
||||
v-for="emoji in customEmojis.filter(
|
||||
|
@ -77,6 +77,7 @@ export default defineComponent({
|
|||
selectedTags: new Set(),
|
||||
searchEmojis: null,
|
||||
i18n,
|
||||
icon,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue