Compare commits

...

46 commits

Author SHA1 Message Date
1aec738200
refactor (client): use ?? to check null/undefined 2024-01-08 17:03:39 +09:00
be726a45f9
chore: upgrade dependencies 2024-01-08 15:16:50 +09:00
bd00d121cb
fix: custom directory doesn't work in containers 2024-01-08 14:58:53 +09:00
daea4247b8
fix: expose icon function 2024-01-08 10:08:39 +09:00
MeiMei
85d5e7bd3b
feat: accept more signature algorithms
enhances 8890902675

Co-authored-by: naskya <m@naskya.net>
2024-01-07 19:53:07 +09:00
1ccf256b99
docker: use mold linker for Rust compilation 2024-01-07 19:03:20 +09:00
acc70d3d80
docker: simplify Rust installation & remove (seemingly) unused build dependencies 2024-01-07 17:55:09 +09:00
54ce89ff9f
docker: trim down installed packages in build stage 2024-01-07 17:30:42 +09:00
41b25228df
dev: append first 2 digits of commit hash to version 2024-01-07 16:50:21 +09:00
81ab8de2fc
fix: add back example docker env file
I just wanted to rename it, didn't mean to remove
2024-01-07 10:51:58 +09:00
80448cd3ac
docker: stop exporting DEBIAN_FRONTEND=noninteractive 2024-01-07 10:36:50 +09:00
6699f6a34f
fix: changelog window overflows the screen if there are too many changes 2024-01-07 10:22:39 +09:00
68285f7165
chore: stop tracking custom directory by git 2024-01-07 07:05:01 +09:00
468073cba8
dev: add Makefile 2024-01-07 07:02:43 +09:00
55514bace4
fix: missing import 2024-01-07 06:39:21 +09:00
e3fc84b80f
fix: typo 2024-01-07 06:26:20 +09:00
0e60607e27
chore: modify the example server config files 2024-01-07 05:58:44 +09:00
7ce992956a
docker: fully qualify image tags in Dockerfile (i.e. add docker.io in front of tags) 2024-01-06 08:07:11 +09:00
01a1a58fa3
fix: moderators being unable to access emoji management page 2024-01-06 00:03:10 +09:00
d12b7cb941
dev: use sadsay for warnings and some sad reports 2024-01-05 14:41:00 +09:00
8a46b92948
refactor: replace confusing expressions relevant to accounts
$i != null -> isSignedIn
$i.isModerator || $i.isAdmin -> isModerator
$i.isAdmin -> isAdmin
$i.emojiModPerm !== "unauthorized" -> isEmojiMod
iAmModerator, iAmAdmin, iAmEmojiMod -> isModerator, isAdmin, isEmojiMod
2024-01-05 14:29:41 +09:00
5ce9d75661
fix: avoid including merge commits in CHANGELOG 2024-01-05 13:22:50 +09:00
17598c2248
dev (minor): add utility function sadsay 2024-01-05 13:18:25 +09:00
17284bbab4
fix (minor): typo 2024-01-05 11:01:20 +09:00
d006fe9190
dev (minor): don't take "yes" as a negative response 2024-01-05 10:37:21 +09:00
5307338de9
chore: upgrade pnpm 2024-01-05 10:17:43 +09:00
657a242b90
feat: ability to publish timelines in signed out UI 2024-01-05 10:15:01 +09:00
d7d9e3c323
dev: support Podman 2024-01-05 08:03:42 +09:00
Lhcfl
8d044c617d
feat: support ChatMessage type (a.k.a. Pleroma chat)
Co-authored-by: naskya <m@naskya.net>
2024-01-05 07:03:30 +09:00
21aa6b097b
docker: migrate image registry to registry.code.naskya.net 2024-01-04 12:23:05 +09:00
Lhcfl
89d058ddc8
fix: fixed post form does not shrink when posting 2024-01-04 11:13:55 +09:00
3a0a2d04f3
dev: prefix message filename with timestamp 2024-01-03 07:28:39 +09:00
27aeca9c2d
fix: unable to translate texts into English
Language code 'en' is deprecated in the DeepL API
2024-01-03 07:28:03 +09:00
23ab5567dd
chore: upgrade dependencies 2024-01-03 00:54:14 +09:00
475e2877f1
chore: remove --dns-result-order=ipv4first
If I recall correctly, this was needed due to unstable connection
with git.joinfirefish.org. We don't get anything from there now.
2024-01-03 00:34:07 +09:00
e81cc3bfdd
chore: format 2024-01-02 14:07:27 +09:00
16b4f78dfb
feat: ability to translate changelog 2024-01-02 14:07:14 +09:00
7f056e729f
refactor: separate translate function into another file & use deepl-node package 2024-01-02 12:31:50 +09:00
11541b1e63
fix: make readFile in api/patrons async 2024-01-02 07:04:25 +09:00
Nanaka Hiira
a09b8e4d80
fix: Cannot display About Firefish page if reaction store is empty 2024-01-02 07:02:52 +09:00
7c887421f4
feat: show changelog on updates 2024-01-02 07:02:52 +09:00
Lhcfl
03807b7815
fix: Missing first autosize when opening long draft 2024-01-02 00:35:10 +09:00
Lhcfl
e8fc5ec37e
fix: unrenote should only delete renotes, not quotes 2024-01-02 00:34:21 +09:00
f0d82ebb94
dev: add clean-npm and clean-cargo, rename clean to clean-built 2024-01-02 00:21:57 +09:00
1d5372282f
dev: update build:debug command 2024-01-02 00:21:56 +09:00
Nanaka Hiira
a53e0d18cf
fix: Show more button does not appear 2024-01-01 20:51:37 +09:00
178 changed files with 2219 additions and 1736 deletions

View file

@ -0,0 +1,4 @@
# db settings
POSTGRES_PASSWORD=very_strong_password
POSTGRES_USER=firefish
POSTGRES_DB=firefish_db

View file

@ -1,4 +0,0 @@
# db settings
POSTGRES_PASSWORD=example-firefish-pass
POSTGRES_USER=example-firefish-user
POSTGRES_DB=firefish

View file

@ -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

View file

@ -42,3 +42,6 @@ packages/backend/assets/instance.css
.git
Dockerfile
docker-compose.yml
# Auto-generated files
/neko/volume

5
.gitignore vendored
View file

@ -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/

View file

@ -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
View 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)" \
.

View file

@ -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))

View file

@ -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

View file

@ -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."

View file

@ -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} でローカルタイムラインとグローバルタイムラインが公開されます。"

View file

@ -801,6 +801,7 @@ useBlurEffect: "在 UI 上使用模糊效果"
learnMore: "更多信息"
misskeyUpdated: "Firefish 更新完成!"
whatIsNew: "显示更新信息"
releaseNotes: "更新信息"
translate: "翻译"
translatedFrom: "从 {x} 翻译"
accountDeletionInProgress: "正在删除账号"

View file

@ -797,6 +797,7 @@ useBlurEffect: "在 UI 上使用模糊效果"
learnMore: "更多資訊"
misskeyUpdated: "Firefish 更新完成!"
whatIsNew: "顯示更新資訊"
releaseNotes: "更新資訊"
translate: "翻譯"
translatedFrom: "從 {x} 翻譯"
accountDeletionInProgress: "正在刪除帳戶"

View 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 |
-------------------------------------------------------------------

View 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 |
---------------------------------------------------

View 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 |
-----------------------------------------------------------

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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
View 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

View file

@ -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
View file

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View file

@ -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"
}
}

View file

@ -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",

View file

@ -379,3 +379,4 @@ export const iso639Regional = {
};
export const langmap = Object.assign({}, langmapNoRegion, iso639Regional);
export type Language = keyof typeof langmap;

View 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),
};
}

View file

@ -61,6 +61,11 @@ export class Meta {
})
public disableGlobalTimeline: boolean;
@Column("boolean", {
default: false,
})
public enableGuestTimeline: boolean;
@Column("varchar", {
length: 256,
default: "⭐",

View file

@ -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)
);
}

View file

@ -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);

View file

@ -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",
},
],
},

View file

@ -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;

View file

@ -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;
}

View file

@ -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],

View file

@ -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,

View file

@ -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;
}

View file

@ -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,

View file

@ -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,
);
});

View file

@ -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);
}
}
});

View file

@ -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,

View file

@ -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: [],
}));

View file

@ -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);
});

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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"
}

View file

@ -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"]) {

View file

@ -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(

View file

@ -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 {

View file

@ -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,

View file

@ -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,

View file

@ -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";

View file

@ -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,

View file

@ -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,

View file

@ -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();
}

View file

@ -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,
}),
});

View file

@ -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>

View file

@ -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>

View file

@ -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(

View file

@ -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>

View file

@ -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

View file

@ -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)),

View file

@ -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))
)

View file

@ -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))
)

View file

@ -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"
/>

View file

@ -64,7 +64,7 @@ const props = withDefaults(
},
);
const includingTypes = computed(() => props.includingTypes || []);
const includingTypes = computed(() => props.includingTypes ?? []);
const dialog = ref<InstanceType<typeof XModalWindow>>();

View file

@ -181,7 +181,7 @@ onMounted(() => {
}
for (
let i = 0;
i < (pagingComponent.value.items || []).length;
i < (pagingComponent.value.items ?? []).length;
i++
) {
if (

View file

@ -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,

View file

@ -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) => {

View file

@ -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());
});
});

View file

@ -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", {

View file

@ -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) {

View file

@ -1,7 +1,7 @@
<template>
<MkEmoji
:emoji="reaction"
:custom-emojis="customEmojis || []"
:custom-emojis="customEmojis ?? []"
:is-reaction="true"
:normal="true"
:no-style="noStyle"

View file

@ -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;

View file

@ -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>

View 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>

View file

@ -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,

View file

@ -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"

View file

@ -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);

View file

@ -341,6 +341,7 @@ function focusFooter(ev) {
&.collapsed,
&.showContent {
display: block;
position: relative;
max-height: calc(15em + 100px);
> .body {

View file

@ -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");
}
};

View file

@ -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"

View file

@ -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);

View file

@ -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>

View file

@ -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 = "";

View file

@ -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;

View file

@ -549,7 +549,8 @@ defineExpose({
}
display: flex;
position: relative;
position: sticky;
top: 0;
z-index: 1;
flex-shrink: 0;
user-select: none;

View file

@ -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>

View file

@ -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"

View file

@ -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,
}),
];

View file

@ -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();
}

View file

@ -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) {

View file

@ -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) => {

View file

@ -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,
}),
);
}

View file

@ -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: {

View file

@ -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",

View file

@ -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({

View file

@ -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