Merge branch 'develop' of https://firefish.dev/firefish/firefish into feat/note-edit-history

This commit is contained in:
Lhcfl 2024-04-01 11:32:14 +08:00
commit e8f178458f
133 changed files with 720 additions and 895 deletions

View file

@ -1,7 +1,5 @@
ifneq (dev,$(wildcard config.env)) include ./dev/config.env
include ./dev/config.env export
export
endif
.PHONY: pre-commit .PHONY: pre-commit
@ -29,13 +27,13 @@ build:
pnpm run migrate pnpm run migrate
.PHONY: db.init db.up db.down .PHONY: db.up db.down db.init
db.init:
$(MAKE) -C ./dev/db-container init
db.up: db.up:
$(MAKE) -C ./dev/db-container up $(MAKE) -C ./dev/db-container up
db.down: db.down:
$(MAKE) -C ./dev/db-container down $(MAKE) -C ./dev/db-container down
db.init:
$(MAKE) -C ./dev/db-container init
.PHONY: psql redis-cli .PHONY: psql redis-cli
psql: psql:

View file

@ -1,9 +1,11 @@
.PHONY: init up down .PHONY: up down init
init: down up
up: up:
$(COMPOSE) up --detach $(COMPOSE) up --detach
down: down:
$(COMPOSE) down $(COMPOSE) down
init:
$(COMPOSE) down --volumes
$(COMPOSE) up --detach
.PHONY: psql redis-cli .PHONY: psql redis-cli
psql: psql:

View file

@ -5,6 +5,8 @@ services:
image: docker.io/redis:7-alpine image: docker.io/redis:7-alpine
ports: ports:
- "26379:6379" - "26379:6379"
volumes:
- "redis-data:/data"
db: db:
image: docker.io/groonga/pgroonga:3.1.8-alpine-12 image: docker.io/groonga/pgroonga:3.1.8-alpine-12
env_file: env_file:
@ -13,3 +15,10 @@ services:
- "25432:5432" - "25432:5432"
volumes: volumes:
- "./install.sql:/docker-entrypoint-initdb.d/install.sql:ro" - "./install.sql:/docker-entrypoint-initdb.d/install.sql:ro"
- "postgres-data:/var/lib/postgresql/data"
volumes:
redis-data:
name: redis-data
postgres-data:
name: postgres-data

View file

@ -5,6 +5,15 @@ Critical security updates are indicated by the :warning: icon.
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well. - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well. - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
## [v20240401](https://firefish.dev/firefish/firefish/-/merge_requests/10724/commits)
- Fix bugs
## :warning: [v20240330](https://firefish.dev/firefish/firefish/-/merge_requests/10719/commits)
- Fix bugs (including a critical security issue)
- We are very thankful to Oneric (the reporter of the security issue) and Laura Hausmann (Iceshrimp maintainer) for kindly and securely sharing the information to fix the issue.
## [v20240326](https://firefish.dev/firefish/firefish/-/merge_requests/10713/commits) ## [v20240326](https://firefish.dev/firefish/firefish/-/merge_requests/10713/commits)
- Fix bugs - Fix bugs

View file

@ -33,6 +33,13 @@ docker pull registry.firefish.dev/firefish/firefish:latest
# or podman pull registry.firefish.dev/firefish/firefish:latest # or podman pull registry.firefish.dev/firefish/firefish:latest
``` ```
## Enable database extension
```sh
docker-compose up db --detach && sleep 5 && docker-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
# or podman-compose up db --detach && sleep 5 && podman-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
```
## Run ## Run
```sh ```sh

View file

@ -35,7 +35,7 @@ There are official installation instructions for many operating systems on <http
##### Ubuntu ##### Ubuntu
1. Add apt repository 1. Install subdependencies and add apt repository
```sh ```sh
sudo apt install -y software-properties-common sudo apt install -y software-properties-common
sudo add-apt-repository -y universe sudo add-apt-repository -y universe
@ -226,7 +226,7 @@ A new setting item has been added to control the log levels, so please consider
### For systemd/pm2 users ### For systemd/pm2 users
- Required Rust version has been bumped from v1.68 to v1.70. - Required Rust version has been bumped from v1.68 to v1.70.
- `libvips` is no longer required (unless your server os is *BSD), so you may uninstall it from your system. Make sure to execute the following commands after that: - `libvips` is no longer required (unless your server OS is *BSD), so you may uninstall it from your system. Make sure to execute the following commands after that:
```sh ```sh
pnpm clean-npm pnpm clean-npm
pnpm install pnpm install

View file

@ -21,8 +21,8 @@ searchUser: Търсене на потребител
reply: Отговор reply: Отговор
showMore: Покажи още showMore: Покажи още
loadMore: Зареди още loadMore: Зареди още
followRequestAccepted: Заявка за последване приета followRequestAccepted: Заявката за последване е приета
importAndExport: Импорт/Експорт на Данни importAndExport: Импорт/експорт на данни
import: Импортиране import: Импортиране
download: Изтегляне download: Изтегляне
export: Експортиране export: Експортиране
@ -47,7 +47,7 @@ groups: Групи
incorrectPassword: Грешна парола. incorrectPassword: Грешна парола.
leaveGroup: Напускане на групата leaveGroup: Напускане на групата
numberOfColumn: Брой колони numberOfColumn: Брой колони
passwordLessLogin: Вход без парола passwordLessLogin: Влизане без парола
newPasswordRetype: Повтори новата парола newPasswordRetype: Повтори новата парола
saveAs: Запазване като... saveAs: Запазване като...
resetPassword: Нулиране на паролата resetPassword: Нулиране на паролата
@ -56,13 +56,13 @@ inputNewFolderName: Въведи ново име на папка
upload: Качване upload: Качване
retypedNotMatch: Въвежданията не съвпадат. retypedNotMatch: Въвежданията не съвпадат.
_ago: _ago:
weeksAgo: преди {n}сед weeksAgo: пр. {n}сед
secondsAgo: преди {n}сек secondsAgo: пр. {n}сек
hoursAgo: преди {n}ч hoursAgo: пр. {n}ч
minutesAgo: преди {n}мин minutesAgo: пр. {n}мин
daysAgo: преди {n}д daysAgo: пр. {n}д
monthsAgo: преди {n}мес monthsAgo: пр. {n}мес
yearsAgo: преди {n}г yearsAgo: пр. {n}г
future: Бъдеще future: Бъдеще
justNow: Току-що justNow: Току-що
folderName: Име на папка folderName: Име на папка
@ -101,6 +101,8 @@ _profile:
metadataLabel: Етикет metadataLabel: Етикет
metadataEdit: Редактиране на допълнителната информация metadataEdit: Редактиране на допълнителната информация
changeAvatar: Промяна на профилната снимка changeAvatar: Промяна на профилната снимка
youCanIncludeHashtags: Можеш също да включиш хаштагове в биографията си.
changeBanner: Промяна на банера
addAccount: Добавяне на акаунт addAccount: Добавяне на акаунт
followRequestPending: Заявка за последване в изчакване followRequestPending: Заявка за последване в изчакване
signinHistory: История на вписванията signinHistory: История на вписванията
@ -108,7 +110,7 @@ or: Или
noUsers: Няма потребители noUsers: Няма потребители
notes: Публикации notes: Публикации
newNoteRecived: Има нови публикации newNoteRecived: Има нови публикации
note: Публикуване note: Публикация
instanceFollowing: Последвани на сървъра instanceFollowing: Последвани на сървъра
_filters: _filters:
followersOnly: Само последователи followersOnly: Само последователи
@ -170,7 +172,7 @@ renotesCount: Брой изпратени подсилвания
license: Лиценз license: Лиценз
lastUsedDate: Последно използвано на lastUsedDate: Последно използвано на
rename: Преименуване rename: Преименуване
customEmojis: Персонализирани емоджита customEmojis: Персон. емоджита
emoji: Емоджи emoji: Емоджи
_aboutFirefish: _aboutFirefish:
translation: Преведи Firefish translation: Преведи Firefish
@ -237,6 +239,7 @@ _theme:
installedThemes: Инсталирани теми installedThemes: Инсталирани теми
constant: Константа constant: Константа
addConstant: Добавяне на константа addConstant: Добавяне на константа
make: Направа на тема
_pages: _pages:
script: script:
blocks: blocks:
@ -369,8 +372,8 @@ enterUsername: Въведи потребителско име
renotedBy: Подсилено от {user} renotedBy: Подсилено от {user}
noNotifications: Няма известия noNotifications: Няма известия
instance: Сървър instance: Сървър
basicSettings: Основни Настройки basicSettings: Основни настройки
otherSettings: Други Настройки otherSettings: Други настройки
openInWindow: Отваряне в прозорец openInWindow: Отваряне в прозорец
profile: Профил profile: Профил
timeline: Инфопоток timeline: Инфопоток
@ -408,7 +411,7 @@ following: Последвани
followsYou: Следва те followsYou: Следва те
createList: Създаване на списък createList: Създаване на списък
error: Грешка error: Грешка
manageLists: Управление на списъци manageLists: Управление на списъците
retry: Повторен опит retry: Повторен опит
follow: Последване follow: Последване
followRequest: Заявка за последване followRequest: Заявка за последване
@ -422,7 +425,7 @@ enterEmoji: Въведи емоджи
sensitive: Деликатно sensitive: Деликатно
add: Добавяне add: Добавяне
pinned: Закачено в профила pinned: Закачено в профила
quote: Цитиране quote: Цитат
pinnedNote: Закачена публикация pinnedNote: Закачена публикация
cantReRenote: Подсилване не може да бъде подсилено. cantReRenote: Подсилване не може да бъде подсилено.
clickToShow: Щракни за показване clickToShow: Щракни за показване
@ -555,7 +558,7 @@ _visibility:
followers: Последователи followers: Последователи
specified: Директна specified: Директна
localOnly: Само местни localOnly: Само местни
public: Публична public: Общодостъпна
publicDescription: Публикацията ще бъде видима във всички публични инфопотоци publicDescription: Публикацията ще бъде видима във всички публични инфопотоци
home: Скрита home: Скрита
localOnlyDescription: Не е видима за отдалечени потребители localOnlyDescription: Не е видима за отдалечени потребители
@ -638,7 +641,7 @@ _preferencesBackups:
updatedAt: 'Обновено на: {date} {time}' updatedAt: 'Обновено на: {date} {time}'
editWidgetsExit: Готово editWidgetsExit: Готово
done: Готово done: Готово
emailRequiredForSignup: Изискване на адрес на ел. поща за регистриране emailRequiredForSignup: Изискване на адрес за ел. поща за регистриране
preview: Преглед preview: Преглед
privacy: Поверителност privacy: Поверителност
about: Относно about: Относно
@ -665,14 +668,14 @@ imageUrl: URL адрес на изображение
announcements: Оповестявания announcements: Оповестявания
removeAreYouSure: Сигурни ли сте, че искате да премахнете "{x}"? removeAreYouSure: Сигурни ли сте, че искате да премахнете "{x}"?
fromUrl: От URL адрес fromUrl: От URL адрес
manageGroups: Управление на групи manageGroups: Управление на групите
nUsersRead: прочетено от {n} nUsersRead: прочетено от {n}
home: Начало home: Начало
registeredDate: Присъединяване registeredDate: Присъединяване
avatar: Профилна снимка avatar: Профилна снимка
watch: Наблюдаване watch: Наблюдаване
antennas: Антени antennas: Антени
manageAntennas: Управление на антени manageAntennas: Управление на антените
popularTags: Популярни тагове popularTags: Популярни тагове
cacheClear: Изчистване на кеша cacheClear: Изчистване на кеша
groupName: Име на групата groupName: Име на групата
@ -700,7 +703,7 @@ gallery: Галерия
priority: Приоритет priority: Приоритет
unread: Непрочетени unread: Непрочетени
filter: Филтриране filter: Филтриране
manageAccounts: Управление на акаунти manageAccounts: Управление на акаунтите
deleteAccount: Изтриване на акаунта deleteAccount: Изтриване на акаунта
fast: Бърза fast: Бърза
remoteOnly: Само отдалечени remoteOnly: Само отдалечени
@ -785,6 +788,9 @@ _menuDisplay:
hide: Скриване hide: Скриване
_exportOrImport: _exportOrImport:
allNotes: Всички публикации allNotes: Всички публикации
followingList: Следвани потребители
blockingList: Блокирани потребители
muteList: Заглушени потребители
exploreFediverse: Разглеждане на Федивселената exploreFediverse: Разглеждане на Федивселената
recentlyUpdatedUsers: Последно активни потребители recentlyUpdatedUsers: Последно активни потребители
uiLanguage: Език на потребителския интерфейс uiLanguage: Език на потребителския интерфейс
@ -793,7 +799,7 @@ tags: Тагове
youHaveNoGroups: Нямаш групи youHaveNoGroups: Нямаш групи
accessibility: Достъпност accessibility: Достъпност
email: Ел. поща email: Ел. поща
emailAddress: Адрес на ел. поща emailAddress: Адрес за ел. поща
addItem: Добавяне на елемент addItem: Добавяне на елемент
visibility: Видимост visibility: Видимост
description: Описание description: Описание
@ -867,3 +873,63 @@ unblockConfirm: Сигурни ли сте, че искате да отблок
followConfirm: Сигурни ли сте, че искате да последвате {name}? followConfirm: Сигурни ли сте, че искате да последвате {name}?
accountMoved: 'Потребителят се премести на нов акаунт:' accountMoved: 'Потребителят се премести на нов акаунт:'
inputNewDescription: Въведете ново описание inputNewDescription: Въведете ново описание
tos: Условия за ползване
agreeTo: Съгласен съм с {0}
withFileAntenna: Само публикации с файлове
updateRemoteUser: Обновяване на инфо. за отдалечения потребител
receiveAnnouncementFromInstance: Получаване на известия от този сървър
userPagePinTip: Можеш да показваш публикации тук, като избереш "Закачане в профила"
от менюто на отделните публикации.
_ffVisibility:
public: Общодостъпна
private: Частна
followers: Видима само за последователи
_charts:
activeUsers: Дейни потребители
edit: Редактиране
toReply: Отговаряне
toPost: Публикуване
toQuote: Цитиране
charts: Диаграми
disablePagesScript: Изключване на AiScript в Страниците
updatedAt: Обновено на
privateDescription: Видима само за теб
enableTimelineStreaming: Автоматично обновяване на инфопотоците
toEdit: Редактиране
showEmojisInReactionNotifications: Показване на емоджита в известията за реакции
rememberNoteVisibility: Запомняне на настройките за видимост на публикациите
drive: Диск
banner: Банер
public: Общодостъпна
makeExplorable: Акаунтът да е видим в "Разглеждане"
hideOnlineStatus: Скриване на онлайн състоянието
customCss: Персонализиран CSS
keepCw: Запазване на предупрежденията за съдържание
makeReactionsPublic: Историята на реакциите да е общодостъпна
noEmailServerWarning: Сървърът за ел. поща не е конфигуриран.
languageForTranslation: Език за превеждане на публикации
private: Частна
replies: Отговори
wordMute: Заглушаване на думи и езици
instanceMute: Заглушаване на сървъри
notificationSettingDesc: Избиране на какви известия да се показват.
preventAiLearning: Предотвратяване на ИИ scraping
indexable: Индексируем
showPreviewByDefault: Показване на преглед при публикуване по подразбиране
showNoAltTextWarning: Показване на предупреждение при опит за публикуване на файлове
без описание
makeFollowManuallyApprove: Заявките за последване да изискват одобряване
enableEmojiReactions: Включване на реакциите с емоджи
autoAcceptFollowed: Автоматично одобряване на заявките за последване от последвани
потребители
expandOnNoteClick: Отваряне на публикацията при кликване
enableInfiniteScroll: Автоматично зареждане на повече
noCrawle: Предотвратяване на индексирането от търсачки
misskeyUpdated: Firefish бе обновен!
emailNotConfiguredWarning: Адресът за ел. поща не е зададен.
notificationSetting: Настройки за известията
emailNotification: Известия по ел. поща
clientSettings: Настройки за устройството
behavior: Поведение
detectPostLanguage: Автоматично откриване на езика и показване на бутон за превеждане
за публикации на чужди езици

View file

@ -856,8 +856,8 @@ more: Més!
featured: Destacat featured: Destacat
usernameOrUserId: Nom o ID d'usuari usernameOrUserId: Nom o ID d'usuari
noSuchUser: No s'ha trobat l'usuari noSuchUser: No s'ha trobat l'usuari
lookup: Cerca lookup: Ves a
attachFile: Afegeix un fitxer attachFile: Afegeix fitxers
currentPassword: Contrasenya actual currentPassword: Contrasenya actual
newPassword: Nova contrasenya newPassword: Nova contrasenya
announcements: Avisos announcements: Avisos
@ -2210,7 +2210,7 @@ squareCatAvatars: Mostrar avatars quadrats per a comptes de gats
replaceChatButtonWithAccountButton: Canviar el botó de xats amb el botó de canvi de replaceChatButtonWithAccountButton: Canviar el botó de xats amb el botó de canvi de
compte compte
replaceWidgetsButtonWithReloadButton: Canviar el botó de ginys amb el botó de recarregar replaceWidgetsButtonWithReloadButton: Canviar el botó de ginys amb el botó de recarregar
searchEngine: Motor de cerca fet servir anla barra MFM searchEngine: Motor de cerca fet servir a la barra MFM
postSearch: Posar els resultats en aquest servidor postSearch: Posar els resultats en aquest servidor
showBigPostButton: Mostrar un botó gegant al formulari de publicació showBigPostButton: Mostrar un botó gegant al formulari de publicació
_emojiModPerm: _emojiModPerm:
@ -2238,7 +2238,7 @@ enableTimelineStreaming: Actualitza les línies de temps automàticament
enablePullToRefresh: Activa "Baixa per actualitzar" enablePullToRefresh: Activa "Baixa per actualitzar"
pullDownToReload: Baixa per actualitzar pullDownToReload: Baixa per actualitzar
pullToRefreshThreshold: Distancia de baixada per actualitzar pullToRefreshThreshold: Distancia de baixada per actualitzar
searchWords: Paraules / ID o adreça a URL a buscar searchWords: Paraules / ID o adreça a URL a cercar
noSentFollowRequests: No tens cap sol·licitud de seguiment enviada noSentFollowRequests: No tens cap sol·licitud de seguiment enviada
sentFollowRequests: Enviar sol·licituds de seguiment sentFollowRequests: Enviar sol·licituds de seguiment
replyMute: Silencia les respostes a les línies de temps replyMute: Silencia les respostes a les línies de temps
@ -2251,7 +2251,7 @@ searchWordsDescription: "Per cercar publicacions, escriu el terme a buscar. Sepa
Si vols cercar per una seqüencia de paraules (per exemple una frase) has d'escriure-les Si vols cercar per una seqüencia de paraules (per exemple una frase) has d'escriure-les
entre cometes dobles, per no fer una cerca amb condicionant AND: \"Avui he aprés\"\ entre cometes dobles, per no fer una cerca amb condicionant AND: \"Avui he aprés\"\
\n \nSi vols anar a una pàgina d'usuari o publicació en concret, escriu la adreça \n \nSi vols anar a una pàgina d'usuari o publicació en concret, escriu la adreça
URL o la ID en aquest camp i fes clic al botó 'Trobar'. Fent clic a 'Cercar' trobarà URL o la ID en aquest camp i fes clic al botó 'Ves a'. Fent clic a 'Cerca' trobarà
publicacions que, literalment , continguin la ID/adreça URL." publicacions que, literalment , continguin la ID/adreça URL."
searchPostsWithFiles: Només publicacions amb fitxers searchPostsWithFiles: Només publicacions amb fitxers
searchCwAndAlt: Inclou avisos de contingut i arxius amb descripcions searchCwAndAlt: Inclou avisos de contingut i arxius amb descripcions

View file

@ -1,6 +1,6 @@
{ {
"name": "firefish", "name": "firefish",
"version": "20240326", "version": "20240401",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://firefish.dev/firefish/firefish.git" "url": "https://firefish.dev/firefish/firefish.git"
@ -20,7 +20,7 @@
"watch": "pnpm run dev", "watch": "pnpm run dev",
"dev": "pnpm node ./scripts/dev.mjs", "dev": "pnpm node ./scripts/dev.mjs",
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start", "dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
"lint": "pnpm -r --parallel run lint", "lint": "pnpm --filter !megalodon --filter !firefish-js -r --parallel run lint",
"debug": "pnpm run build:debug && pnpm run start", "debug": "pnpm run build:debug && pnpm run start",
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp", "build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
"mocha": "pnpm --filter backend run mocha", "mocha": "pnpm --filter backend run mocha",

View file

@ -4,7 +4,6 @@ Cargo.lock
.github .github
npm npm
.eslintrc .eslintrc
.prettierignore
rustfmt.toml rustfmt.toml
yarn.lock yarn.lock
*.node *.node

View file

@ -8,10 +8,15 @@ import chalk from "chalk";
import Logger from "@/services/logger.js"; import Logger from "@/services/logger.js";
import IPCIDR from "ip-cidr"; import IPCIDR from "ip-cidr";
import PrivateIp from "private-ip"; import PrivateIp from "private-ip";
import { isValidUrl } from "./is-valid-url.js";
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
export async function downloadUrl(url: string, path: string): Promise<void> { export async function downloadUrl(url: string, path: string): Promise<void> {
if (!isValidUrl(url)) {
throw new StatusError("Invalid URL", 400);
}
const logger = new Logger("download"); const logger = new Logger("download");
logger.info(`Downloading ${chalk.cyan(url)} ...`); logger.info(`Downloading ${chalk.cyan(url)} ...`);
@ -44,6 +49,12 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
limit: 0, limit: 0,
}, },
}) })
.on("redirect", (res: Got.Response, opts: Got.NormalizedOptions) => {
if (!isValidUrl(opts.url)) {
logger.warn(`Invalid URL: ${opts.url}`);
req.destroy();
}
})
.on("response", (res: Got.Response) => { .on("response", (res: Got.Response) => {
if ( if (
(process.env.NODE_ENV === "production" || (process.env.NODE_ENV === "production" ||

View file

@ -2,9 +2,10 @@ import * as http from "node:http";
import * as https from "node:https"; import * as https from "node:https";
import type { URL } from "node:url"; import type { URL } from "node:url";
import CacheableLookup from "cacheable-lookup"; import CacheableLookup from "cacheable-lookup";
import fetch from "node-fetch"; import fetch, { RequestRedirect } from "node-fetch";
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent"; import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
import config from "@/config/index.js"; import config from "@/config/index.js";
import { isValidUrl } from "./is-valid-url.js";
export async function getJson( export async function getJson(
url: string, url: string,
@ -57,7 +58,12 @@ export async function getResponse(args: {
headers: Record<string, string>; headers: Record<string, string>;
timeout?: number; timeout?: number;
size?: number; size?: number;
redirect?: RequestRedirect;
}) { }) {
if (!isValidUrl(args.url)) {
throw new StatusError("Invalid URL", 400);
}
const timeout = args.timeout || 10 * 1000; const timeout = args.timeout || 10 * 1000;
const controller = new AbortController(); const controller = new AbortController();
@ -73,8 +79,16 @@ export async function getResponse(args: {
size: args.size || 10 * 1024 * 1024, size: args.size || 10 * 1024 * 1024,
agent: getAgentByUrl, agent: getAgentByUrl,
signal: controller.signal, signal: controller.signal,
redirect: args.redirect,
}); });
if (args.redirect === "manual" && [301, 302, 307, 308].includes(res.status)) {
if (!isValidUrl(res.url)) {
throw new StatusError("Invalid URL", 400);
}
return res;
}
if (!res.ok) { if (!res.ok) {
throw new StatusError( throw new StatusError(
`${res.status} ${res.statusText}`, `${res.status} ${res.statusText}`,

View file

@ -0,0 +1,20 @@
export function isValidUrl(url: string | URL | undefined): boolean {
if (process.env.NODE_ENV !== "production") return true;
try {
if (url == null) return false;
const u = typeof url === "string" ? new URL(url) : url;
if (!u.protocol.match(/^https?:$/) || u.hostname === "unix") {
return false;
}
if (u.port !== "" && !["80", "443"].includes(u.port)) {
return false;
}
return true;
} catch {
return false;
}
}

View file

@ -1,10 +1,12 @@
import config from "@/config/index.js"; import config from "@/config/index.js";
import { getUserKeypair } from "@/misc/keypair-store.js"; import { getUserKeypair } from "@/misc/keypair-store.js";
import type { User, ILocalUser } from "@/models/entities/user.js"; import type { User, ILocalUser } from "@/models/entities/user.js";
import { getResponse } from "@/misc/fetch.js"; import { StatusError, getResponse } from "@/misc/fetch.js";
import { createSignedPost, createSignedGet } from "./ap-request.js"; import { createSignedPost, createSignedGet } from "./ap-request.js";
import type { Response } from "node-fetch"; import type { Response } from "node-fetch";
import type { IObject } from "./type.js"; import type { IObject } from "./type.js";
import { isValidUrl } from "@/misc/is-valid-url.js";
import { apLogger } from "@/remote/activitypub/logger.js";
export default async (user: { id: User["id"] }, url: string, object: any) => { export default async (user: { id: User["id"] }, url: string, object: any) => {
const body = JSON.stringify(object); const body = JSON.stringify(object);
@ -33,10 +35,19 @@ export default async (user: { id: User["id"] }, url: string, object: any) => {
/** /**
* Get ActivityPub object * Get ActivityPub object
* @param user http-signature user
* @param url URL to fetch * @param url URL to fetch
* @param user http-signature user
* @param redirects whether or not to accept redirects
*/ */
export async function apGet(url: string, user?: ILocalUser): Promise<IObject> { export async function apGet(
url: string,
user?: ILocalUser,
redirects: boolean = true,
): Promise<{ finalUrl: string; content: IObject }> {
if (!isValidUrl(url)) {
throw new StatusError("Invalid URL", 400);
}
let res: Response; let res: Response;
if (user != null) { if (user != null) {
@ -56,7 +67,16 @@ export async function apGet(url: string, user?: ILocalUser): Promise<IObject> {
url, url,
method: req.request.method, method: req.request.method,
headers: req.request.headers, headers: req.request.headers,
redirect: redirects ? "manual" : "error",
}); });
if (redirects && [301, 302, 307, 308].includes(res.status)) {
const newUrl = res.headers.get("location");
if (newUrl == null)
throw new Error("apGet got redirect but no target location");
apLogger.debug(`apGet is redirecting to ${newUrl}`);
return apGet(newUrl, user, false);
}
} else { } else {
res = await getResponse({ res = await getResponse({
url, url,
@ -66,12 +86,23 @@ export async function apGet(url: string, user?: ILocalUser): Promise<IObject> {
'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
"User-Agent": config.userAgent, "User-Agent": config.userAgent,
}, },
redirect: redirects ? "manual" : "error",
}); });
if (redirects && [301, 302, 307, 308].includes(res.status)) {
const newUrl = res.headers.get("location");
if (newUrl == null)
throw new Error("apGet got redirect but no target location");
apLogger.debug(`apGet is redirecting to ${newUrl}`);
return apGet(newUrl, undefined, false);
}
} }
const contentType = res.headers.get("content-type"); const contentType = res.headers.get("content-type");
if (contentType == null || !validateContentType(contentType)) { if (contentType == null || !validateContentType(contentType)) {
throw new Error("Invalid Content Type"); throw new Error(
`apGet response had unexpected content-type: ${contentType}`,
);
} }
if (res.body == null) throw new Error("body is null"); if (res.body == null) throw new Error("body is null");
@ -79,7 +110,10 @@ export async function apGet(url: string, user?: ILocalUser): Promise<IObject> {
const text = await res.text(); const text = await res.text();
if (text.length > 65536) throw new Error("too big result"); if (text.length > 65536) throw new Error("too big result");
return JSON.parse(text) as IObject; return {
finalUrl: res.url,
content: JSON.parse(text) as IObject,
};
} }
function validateContentType(contentType: string): boolean { function validateContentType(contentType: string): boolean {

View file

@ -6,7 +6,13 @@ import { extractDbHost, isSelfHost } from "@/misc/convert-host.js";
import { apGet } from "./request.js"; import { apGet } from "./request.js";
import type { IObject, ICollection, IOrderedCollection } from "./type.js"; import type { IObject, ICollection, IOrderedCollection } from "./type.js";
import { isCollectionOrOrderedCollection, getApId } from "./type.js"; import { isCollectionOrOrderedCollection, getApId } from "./type.js";
import { Notes, NoteReactions, Polls, Users } from "@/models/index.js"; import {
FollowRequests,
Notes,
NoteReactions,
Polls,
Users,
} from "@/models/index.js";
import { parseUri } from "./db-resolver.js"; import { parseUri } from "./db-resolver.js";
import renderNote from "@/remote/activitypub/renderer/note.js"; import renderNote from "@/remote/activitypub/renderer/note.js";
import { renderLike } from "@/remote/activitypub/renderer/like.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js";
@ -17,7 +23,7 @@ import { renderActivity } from "@/remote/activitypub/renderer/index.js";
import renderFollow from "@/remote/activitypub/renderer/follow.js"; import renderFollow from "@/remote/activitypub/renderer/follow.js";
import { shouldBlockInstance } from "@/misc/should-block-instance.js"; import { shouldBlockInstance } from "@/misc/should-block-instance.js";
import { apLogger } from "@/remote/activitypub/logger.js"; import { apLogger } from "@/remote/activitypub/logger.js";
import { In, IsNull, Not } from "typeorm"; import { IsNull, Not } from "typeorm";
export default class Resolver { export default class Resolver {
private history: Set<string>; private history: Set<string>;
@ -114,7 +120,7 @@ export default class Resolver {
apLogger.debug("Getting object from remote, authenticated as user:"); apLogger.debug("Getting object from remote, authenticated as user:");
apLogger.debug(JSON.stringify(this.user, null, 2)); apLogger.debug(JSON.stringify(this.user, null, 2));
const object = await apGet(value, this.user); const { finalUrl, content: object } = await apGet(value, this.user);
if ( if (
object == null || object == null ||
@ -127,61 +133,73 @@ export default class Resolver {
throw new Error("invalid response"); throw new Error("invalid response");
} }
return object; if (object.id == null) {
throw new Error("Object has no ID");
}
if (finalUrl === object.id) return object;
if (new URL(finalUrl).host !== new URL(object.id).host) {
throw new Error("Object ID host doesn't match final url host");
}
const finalRes = await apGet(object.id, this.user);
if (finalRes.finalUrl !== finalRes.content.id)
throw new Error(
"Object ID still doesn't match final URL after second fetch attempt",
);
return finalRes.content;
} }
private resolveLocal(url: string): Promise<IObject> { private async resolveLocal(url: string): Promise<IObject> {
const parsed = parseUri(url); const parsed = parseUri(url);
if (!parsed.local) throw new Error("resolveLocal: not local"); if (!parsed.local) throw new Error("resolveLocal: not local");
switch (parsed.type) { switch (parsed.type) {
case "notes": case "notes":
return Notes.findOneByOrFail({ id: parsed.id }).then((note) => { const note = await Notes.findOneByOrFail({ id: parsed.id });
if (parsed.rest === "activity") { if (parsed.rest === "activity") {
// this refers to the create activity and not the note itself // this refers to the create activity and not the note itself
return renderActivity(renderCreate(renderNote(note), note)); return renderActivity(renderCreate(renderNote(note), note));
} else { } else {
return renderNote(note); return renderNote(note);
} }
});
case "users": case "users":
return Users.findOneByOrFail({ id: parsed.id }).then((user) => const user = await Users.findOneByOrFail({ id: parsed.id });
renderPerson(user as ILocalUser), return await renderPerson(user as ILocalUser);
);
case "questions": case "questions":
// Polls are indexed by the note they are attached to. // Polls are indexed by the note they are attached to.
return Promise.all([ const [pollNote, poll] = await Promise.all([
Notes.findOneByOrFail({ id: parsed.id }), Notes.findOneByOrFail({ id: parsed.id }),
Polls.findOneByOrFail({ noteId: parsed.id }), Polls.findOneByOrFail({ noteId: parsed.id }),
]).then(([note, poll]) => ]);
renderQuestion({ id: note.userId }, note, poll), return await renderQuestion({ id: pollNote.userId }, pollNote, poll);
);
case "likes": case "likes":
return NoteReactions.findOneByOrFail({ id: parsed.id }).then( const reaction = await NoteReactions.findOneByOrFail({ id: parsed.id });
(reaction) => renderActivity(renderLike(reaction, { uri: null })), return renderActivity(renderLike(reaction, { uri: null }));
);
case "follows": case "follows":
// if rest is a <followee id> // if rest is a <followee id>
if (parsed.rest != null && /^\w+$/.test(parsed.rest)) { if (parsed.rest != null && /^\w+$/.test(parsed.rest)) {
return Promise.all( const [follower, followee] = await Promise.all(
[parsed.id, parsed.rest].map((id) => Users.findOneByOrFail({ id })), [parsed.id, parsed.rest].map((id) => Users.findOneByOrFail({ id })),
).then(([follower, followee]) =>
renderActivity(renderFollow(follower, followee, url)),
); );
return renderActivity(renderFollow(follower, followee, url));
} }
// Another situation is there is only requestId, then obtained object from database. // Another situation is there is only requestId, then obtained object from database.
const followRequest = FollowRequests.findOneBy({ const followRequest = await FollowRequests.findOneBy({
id: parsed.id, id: parsed.id,
}); });
if (followRequest == null) { if (followRequest == null) {
throw new Error("resolveLocal: invalid follow URI"); throw new Error("resolveLocal: invalid follow URI");
} }
const follower = Users.findOneBy({ const follower = await Users.findOneBy({
id: followRequest.followerId, id: followRequest.followerId,
host: IsNull(), host: IsNull(),
}); });
const followee = Users.findOneBy({ const followee = await Users.findOneBy({
id: followRequest.followeeId, id: followRequest.followeeId,
host: Not(IsNull()), host: Not(IsNull()),
}); });
@ -190,7 +208,7 @@ export default class Resolver {
} }
return renderActivity(renderFollow(follower, followee, url)); return renderActivity(renderFollow(follower, followee, url));
default: default:
throw new Error(`resolveLocal: type ${type} unhandled`); throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
} }
} }
} }

View file

@ -54,6 +54,8 @@ export default async function (ctx: Koa.Context) {
return; return;
} }
ctx.set("X-Content-Type-Options", "nosniff");
const isThumbnail = file.thumbnailAccessKey === key; const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key; const isWebpublic = file.webpublicAccessKey === key;

View file

@ -1,8 +1,19 @@
{ {
"extends": ["@eslint-sets/vue3", "@eslint-sets/vue3-ts"], "extends": ["@eslint-sets/vue3", "@eslint-sets/vue3-ts"],
"plugins": ["file-progress", "prettier"], "plugins": ["file-progress"],
"ignorePatterns": ["**/*.json5"], "ignorePatterns": ["**/*.json5"],
"rules": { "rules": {
"file-progress/activate": 1 "file-progress/activate": 1,
"prettier/prettier": 0,
"one-var": ["error", "never"],
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
}
]
} }
} }

View file

@ -8,7 +8,7 @@
"build:debug": "pnpm run build", "build:debug": "pnpm run build",
"lint": "pnpm biome check **/*.ts --apply ; pnpm run lint:vue", "lint": "pnpm biome check **/*.ts --apply ; pnpm run lint:vue",
"lint:vue": "pnpm eslint src --fix '**/*.vue' --cache ; pnpm run format", "lint:vue": "pnpm eslint src --fix '**/*.vue' --cache ; pnpm run format",
"format": "pnpm biome format * --write && pnpm prettier --write '**/*.{scss,vue}' --cache --cache-strategy metadata" "format": "pnpm biome format * --write"
}, },
"devDependencies": { "devDependencies": {
"@eslint-sets/eslint-config-vue3": "^5.12.0", "@eslint-sets/eslint-config-vue3": "^5.12.0",
@ -48,7 +48,6 @@
"cropperjs": "2.0.0-beta.4", "cropperjs": "2.0.0-beta.4",
"date-fns": "3.6.0", "date-fns": "3.6.0",
"emojilib": "^3.0.11", "emojilib": "^3.0.11",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-file-progress": "^1.3.0", "eslint-plugin-file-progress": "^1.3.0",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"fast-blurhash": "^1.1.2", "fast-blurhash": "^1.1.2",
@ -65,7 +64,6 @@
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"moment": "2.30.1", "moment": "2.30.1",
"photoswipe": "5.4.3", "photoswipe": "5.4.3",
"prettier": "3.2.5",
"prismjs": "1.29.0", "prismjs": "1.29.0",
"punycode": "2.3.1", "punycode": "2.3.1",
"rollup": "4.13.0", "rollup": "4.13.0",

View file

@ -179,8 +179,7 @@ async function renderActiveUsersChart() {
enabled: false, enabled: false,
callbacks: { callbacks: {
title(context) { title(context) {
const v = const v = context[0].dataset.data[context[0].dataIndex];
context[0].dataset.data[context[0].dataIndex];
return v.d; return v.d;
}, },
label(context) { label(context) {

View file

@ -166,21 +166,21 @@ const texts = computed(() => {
return angles; return angles;
}); });
let enabled = true, let enabled = true;
majorGraduationColor = ref<string>(), const majorGraduationColor = ref<string>();
// let minorGraduationColor = $ref<string>(); // let minorGraduationColor = $ref<string>();
sHandColor = ref<string>(), const sHandColor = ref<string>();
mHandColor = ref<string>(), const mHandColor = ref<string>();
hHandColor = ref<string>(), const hHandColor = ref<string>();
nowColor = ref<string>(), const nowColor = ref<string>();
h = ref<number>(0), const h = ref<number>(0);
m = ref<number>(0), const m = ref<number>(0);
s = ref<number>(0), const s = ref<number>(0);
hAngle = ref<number>(0), const hAngle = ref<number>(0);
mAngle = ref<number>(0), const mAngle = ref<number>(0);
sAngle = ref<number>(0), const sAngle = ref<number>(0);
disableSAnimate = ref(false), const disableSAnimate = ref(false);
sOneRound = false; let sOneRound = false;
function tick() { function tick() {
const now = new Date(); const now = new Date();

View file

@ -128,20 +128,20 @@ const getColor = (i) => {
}; };
const now = new Date(); const now = new Date();
let chartInstance: Chart = null, let chartInstance: Chart = null;
chartData: { let chartData: {
series: { series: {
name: string; name: string;
type: "line" | "area"; type: "line" | "area";
color?: string; color?: string;
dashed?: boolean; dashed?: boolean;
hidden?: boolean; hidden?: boolean;
data: { data: {
x: number; x: number;
y: number; y: number;
}[];
}[]; }[];
} = null; }[];
} = null;
const chartEl = ref<HTMLCanvasElement>(null); const chartEl = ref<HTMLCanvasElement>(null);
const fetching = ref(true); const fetching = ref(true);

View file

@ -33,8 +33,8 @@ const rootEl = ref<HTMLDivElement>();
const zIndex = ref<number>(os.claimZIndex("high")); const zIndex = ref<number>(os.claimZIndex("high"));
onMounted(() => { onMounted(() => {
let left = props.ev.pageX + 1, // + 1 let left = props.ev.pageX + 1; // + 1
top = props.ev.pageY + 1; // + 1 let top = props.ev.pageY + 1; // + 1
const width = rootEl.value.offsetWidth; const width = rootEl.value.offsetWidth;
const height = rootEl.value.offsetHeight; const height = rootEl.value.offsetHeight;

View file

@ -64,8 +64,8 @@ const imgUrl = `${url}/proxy/image.webp?${query({
})}`; })}`;
const dialogEl = ref<InstanceType<typeof XModalWindow>>(); const dialogEl = ref<InstanceType<typeof XModalWindow>>();
const imgEl = ref<HTMLImageElement>(); const imgEl = ref<HTMLImageElement>();
let cropper: Cropper | null = null, let cropper: Cropper | null = null;
loading = ref(true); const loading = ref(true);
const ok = async () => { const ok = async () => {
const promise = new Promise<entities.DriveFile>(async (res) => { const promise = new Promise<entities.DriveFile>(async (res) => {

View file

@ -83,9 +83,7 @@ function getMenu() {
text: props.file.isSensitive text: props.file.isSensitive
? i18n.ts.unmarkAsSensitive ? i18n.ts.unmarkAsSensitive
: i18n.ts.markAsSensitive, : i18n.ts.markAsSensitive,
icon: props.file.isSensitive icon: props.file.isSensitive ? "ph-eye ph-lg" : "ph-eye-slash ph-lg",
? "ph-eye ph-lg"
: "ph-eye-slash ph-lg",
action: toggleSensitive, action: toggleSensitive,
}, },
{ {
@ -129,9 +127,7 @@ function onClick(ev: MouseEvent) {
} else { } else {
os.popupMenu( os.popupMenu(
getMenu(), getMenu(),
(ev.currentTarget ?? ev.target ?? undefined) as (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined,
| HTMLElement
| undefined,
); );
} }
} }

View file

@ -451,9 +451,7 @@ function chooseFile(file: entities.DriveFile) {
const isAlreadySelected = selectedFiles.value.some((f) => f.id === file.id); const isAlreadySelected = selectedFiles.value.some((f) => f.id === file.id);
if (props.multiple) { if (props.multiple) {
if (isAlreadySelected) { if (isAlreadySelected) {
selectedFiles.value = selectedFiles.value.filter( selectedFiles.value = selectedFiles.value.filter((f) => f.id !== file.id);
(f) => f.id !== file.id,
);
} else { } else {
selectedFiles.value.push(file); selectedFiles.value.push(file);
} }

View file

@ -113,19 +113,19 @@
<header>{{ i18n.ts.emoji }}</header> <header>{{ i18n.ts.emoji }}</header>
<XSection <XSection
v-for="category in unicodeEmojiCategories" v-for="category in unicodeEmojiCategories"
:key="category" :key="category.slug"
:skin-tone-selector="category === 'people'" :skin-tone-selector="category.slug === 'people_body'"
:skin-tones="unicodeEmojiSkinTones" :skin-tones="unicodeEmojiSkinTones"
:skin-tone-labels="unicodeEmojiSkinToneLabels" :skin-tone-labels="unicodeEmojiSkinToneLabels"
:emojis=" :emojis="
emojilist emojilist
.filter((e) => e.category === category) .filter(
(e) => e.category_slug === category.slug,
)
.map((e) => e.emoji) .map((e) => e.emoji)
" "
@chosen="chosen" @chosen="chosen"
>{{ >{{ category.name }}</XSection
getNicelyLabeledCategory(category) || category
}}</XSection
> >
</div> </div>
</div> </div>
@ -169,11 +169,7 @@ import type { entities } from "firefish-js";
import { FocusTrap } from "focus-trap-vue"; import { FocusTrap } from "focus-trap-vue";
import XSection from "@/components/MkEmojiPicker.section.vue"; import XSection from "@/components/MkEmojiPicker.section.vue";
import type { UnicodeEmojiDef } from "@/scripts/emojilist"; import type { UnicodeEmojiDef } from "@/scripts/emojilist";
import { import { emojilist, unicodeEmojiCategories } from "@/scripts/emojilist";
emojilist,
getNicelyLabeledCategory,
unicodeEmojiCategories,
} from "@/scripts/emojilist";
import { getStaticImageUrl } from "@/scripts/get-static-image-url"; import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import Ripple from "@/components/MkRipple.vue"; import Ripple from "@/components/MkRipple.vue";
import * as os from "@/os"; import * as os from "@/os";
@ -284,9 +280,7 @@ watch(q, () => {
keywords.every( keywords.every(
(keyword) => (keyword) =>
emoji.name.includes(keyword) || emoji.name.includes(keyword) ||
emoji.aliases.some((alias) => emoji.aliases.some((alias) => alias.includes(keyword)),
alias.includes(keyword),
),
) )
) { ) {
matches.add(emoji); matches.add(emoji);
@ -356,9 +350,7 @@ watch(q, () => {
keywords.every( keywords.every(
(keyword) => (keyword) =>
emoji.slug.includes(keyword) || emoji.slug.includes(keyword) ||
emoji.keywords?.some((alias) => emoji.keywords?.some((alias) => alias.includes(keyword)),
alias.includes(keyword),
),
) )
) { ) {
matches.add(emoji); matches.add(emoji);
@ -375,9 +367,7 @@ watch(q, () => {
if (matches.size >= max) return matches; if (matches.size >= max) return matches;
for (const emoji of emojis) { for (const emoji of emojis) {
if ( if (emoji.keywords?.some((keyword) => keyword.startsWith(newQ))) {
emoji.keywords?.some((keyword) => keyword.startsWith(newQ))
) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -428,8 +418,7 @@ function getKey(
function chosen(emoji: any, ev?: MouseEvent) { function chosen(emoji: any, ev?: MouseEvent) {
const el = const el =
ev && ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2; const x = rect.left + el.offsetWidth / 2;

View file

@ -58,9 +58,7 @@ export default defineComponent({
showBody: showBody:
this.persistKey && this.persistKey &&
localStorage.getItem(localStoragePrefix + this.persistKey) localStorage.getItem(localStoragePrefix + this.persistKey)
? localStorage.getItem( ? localStorage.getItem(localStoragePrefix + this.persistKey) === "t"
localStoragePrefix + this.persistKey,
) === "t"
: this.expanded, : this.expanded,
animation: defaultStore.state.animation, animation: defaultStore.state.animation,
}; };

View file

@ -112,8 +112,7 @@ if (props.user.isFollowing == null) {
function onFollowChange(user: entities.UserDetailed) { function onFollowChange(user: entities.UserDetailed) {
if (user.id === props.user.id) { if (user.id === props.user.id) {
isFollowing.value = user.isFollowing; isFollowing.value = user.isFollowing;
hasPendingFollowRequestFromYou.value = hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
user.hasPendingFollowRequestFromYou;
} }
} }
@ -172,10 +171,7 @@ async function onClick() {
} }
function menu(ev) { function menu(ev) {
os.popupMenu( os.popupMenu(getUserMenu(props.user, router), ev.currentTarget ?? ev.target);
getUserMenu(props.user, router),
ev.currentTarget ?? ev.target,
);
} }
onMounted(() => { onMounted(() => {

View file

@ -22,14 +22,14 @@
@load="onLoad" @load="onLoad"
/> />
<i <i
class="alt-indicator"
:class="icon('ph-subtitles')"
v-if="alt && showAltIndicator" v-if="alt && showAltIndicator"
v-tooltip.noLabel=" v-tooltip.noLabel="
`${i18n.ts.alt}: ${ `${i18n.ts.alt}: ${
alt.length > 200 ? alt.trim().slice(0, 200) + '...' : alt.trim() alt.length > 200 ? alt.trim().slice(0, 200) + '...' : alt.trim()
}` }`
" "
class="alt-indicator"
:class="icon('ph-subtitles')"
></i> ></i>
</template> </template>

View file

@ -38,9 +38,7 @@ const instance = props.instance ?? {
faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico", faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico",
name: instanceName, name: instanceName,
themeColor: ( themeColor: (
document.querySelector( document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement
'meta[name="theme-color-orig"]',
) as HTMLMetaElement
)?.content, )?.content,
softwareName: Instance.softwareName ?? "Firefish", softwareName: Instance.softwareName ?? "Firefish",
softwareVersion: version, softwareVersion: version,

View file

@ -40,9 +40,7 @@ const el = ref();
useTooltip(el, (showing) => { useTooltip(el, (showing) => {
os.popup( os.popup(
defineAsyncComponent( defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
() => import("@/components/MkUrlPreviewPopup.vue"),
),
{ {
showing, showing,
url: props.url, url: props.url,

View file

@ -58,9 +58,7 @@ export default {
{ {
class: $style.text, class: $style.text,
style: { style: {
animationDirection: reverse animationDirection: reverse ? "reverse" : undefined,
? "reverse"
: undefined,
}, },
}, },
$slots.default(), $slots.default(),

View file

@ -113,7 +113,7 @@ const url =
props.raw || defaultStore.state.loadRawImages props.raw || defaultStore.state.loadRawImages
? props.media.url ? props.media.url
: defaultStore.state.disableShowingAnimatedImages && : defaultStore.state.disableShowingAnimatedImages &&
props.media.type.startsWith("image") props.media.type.startsWith("image")
? getStaticImageUrl(props.media.thumbnailUrl) ? getStaticImageUrl(props.media.thumbnailUrl)
: props.media.thumbnailUrl; : props.media.thumbnailUrl;
@ -150,8 +150,7 @@ watch(
hide.value = hide.value =
defaultStore.state.nsfw === "force" defaultStore.state.nsfw === "force"
? true ? true
: props.media.isSensitive && : props.media.isSensitive && defaultStore.state.nsfw !== "ignore";
defaultStore.state.nsfw !== "ignore";
}, },
{ {
deep: true, deep: true,

View file

@ -75,8 +75,7 @@ const audioEl = ref<HTMLAudioElement | null>();
const hide = ref(true); const hide = ref(true);
function volumechange() { function volumechange() {
if (audioEl.value) if (audioEl.value) ColdDeviceStorage.set("mediaVolume", audioEl.value.volume);
ColdDeviceStorage.set("mediaVolume", audioEl.value.volume);
} }
onMounted(() => { onMounted(() => {

View file

@ -183,12 +183,8 @@ export default defineComponent({
}, },
caption() { caption() {
const img = document.getElementById( const img = document.getElementById("imgtocaption") as HTMLImageElement;
"imgtocaption", const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
) as HTMLImageElement;
const ta = document.getElementById(
"captioninput",
) as HTMLTextAreaElement;
os.api("drive/files/caption-image", { os.api("drive/files/caption-image", {
url: img.src, url: img.src,
}).then((text) => { }).then((text) => {

View file

@ -168,10 +168,10 @@ function load() {
onMounted(load); onMounted(load);
let currentRow = 0, let currentRow = 0;
rowHeight = 0, let rowHeight = 0;
buffer = null, let buffer = null;
isSeeking = false; const isSeeking = false;
function captionPopup() { function captionPopup() {
os.alert({ os.alert({
@ -311,11 +311,7 @@ function getRow(pattern: number, rowOffset: number) {
const ops: string[] = []; const ops: string[] = [];
for (let channel = 0; channel < nbChannels.value; channel++) { for (let channel = 0; channel < nbChannels.value; channel++) {
const part = player.value.getPatternRowChannel( const part = player.value.getPatternRowChannel(pattern, rowOffset, channel);
pattern,
rowOffset,
channel,
);
notes.push(part.substring(0, 3)); notes.push(part.substring(0, 3));
insts.push(part.substring(4, 6)); insts.push(part.substring(4, 6));

View file

@ -235,7 +235,8 @@ const align = () => {
const width = content.value!.offsetWidth; const width = content.value!.offsetWidth;
const height = content.value!.offsetHeight; const height = content.value!.offsetHeight;
let left, top; let left;
let top;
const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset); const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset); const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
@ -286,8 +287,7 @@ const align = () => {
left = window.innerWidth - width + window.scrollX - 1; left = window.innerWidth - width + window.scrollX - 1;
} }
const underSpace = const underSpace = window.innerHeight - MARGIN - (top - window.pageYOffset);
window.innerHeight - MARGIN - (top - window.pageYOffset);
const upperSpace = srcRect.top - MARGIN; const upperSpace = srcRect.top - MARGIN;
// //
@ -300,12 +300,7 @@ const align = () => {
top = window.scrollY + (upperSpace + MARGIN - height); top = window.scrollY + (upperSpace + MARGIN - height);
} }
} else { } else {
top = top = window.innerHeight - MARGIN - height + window.pageYOffset - 1;
window.innerHeight -
MARGIN -
height +
window.pageYOffset -
1;
} }
} else { } else {
maxHeight.value = underSpace; maxHeight.value = underSpace;
@ -320,8 +315,8 @@ const align = () => {
left = 0; left = 0;
} }
let transformOriginX = "center", let transformOriginX = "center";
transformOriginY = "center"; let transformOriginY = "center";
if ( if (
top >= top >=

View file

@ -412,8 +412,7 @@ async function translate() {
lang != null && lang != null &&
translateLang !== lang && translateLang !== lang &&
(!translation.value || (!translation.value ||
translation.value.sourceLang.toLowerCase() === translation.value.sourceLang.toLowerCase() === translateLang.slice(0, 2))
translateLang.slice(0, 2))
) )
translation.value = await translate_(appearNote.value.id, lang); translation.value = await translate_(appearNote.value.id, lang);
translating.value = false; translating.value = false;
@ -515,10 +514,7 @@ function onContextmenu(ev: MouseEvent): void {
icon: `${icon("ph-arrows-out-simple")}`, icon: `${icon("ph-arrows-out-simple")}`,
text: i18n.ts.showInPage, text: i18n.ts.showInPage,
action: () => { action: () => {
router.push( router.push(notePage(appearNote.value), "forcePage");
notePage(appearNote.value),
"forcePage",
);
}, },
} }
: undefined, : undefined,
@ -542,10 +538,7 @@ function onContextmenu(ev: MouseEvent): void {
type: "a", type: "a",
icon: `${icon("ph-arrow-square-up-right")}`, icon: `${icon("ph-arrow-square-up-right")}`,
text: i18n.ts.showOnRemote, text: i18n.ts.showOnRemote,
href: href: appearNote.value.url ?? appearNote.value.uri ?? "",
appearNote.value.url ??
appearNote.value.uri ??
"",
target: "_blank", target: "_blank",
} }
: undefined, : undefined,

View file

@ -70,8 +70,7 @@ const note = ref(props.note);
const showTicker = const showTicker =
defaultStore.state.instanceTicker === "always" || defaultStore.state.instanceTicker === "always" ||
(defaultStore.state.instanceTicker === "remote" && (defaultStore.state.instanceTicker === "remote" && note.value.user.instance);
note.value.user.instance);
function openServerInfo() { function openServerInfo() {
if (!props.canOpenServerInfo || !defaultStore.state.openServerInfo) return; if (!props.canOpenServerInfo || !defaultStore.state.openServerInfo) return;

View file

@ -280,8 +280,7 @@ const replies: entities.Note[] =
props.conversation props.conversation
?.filter( ?.filter(
(item) => (item) =>
item.replyId === props.note.id || item.replyId === props.note.id || item.renoteId === props.note.id,
item.renoteId === props.note.id,
) )
.reverse() ?? []; .reverse() ?? [];
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
@ -319,8 +318,7 @@ async function translate() {
lang != null && lang != null &&
translateLang !== lang && translateLang !== lang &&
(!translation.value || (!translation.value ||
translation.value.sourceLang.toLowerCase() === translation.value.sourceLang.toLowerCase() === translateLang.slice(0, 2))
translateLang.slice(0, 2))
) )
translation.value = await translate_(appearNote.value.id, lang); translation.value = await translate_(appearNote.value.id, lang);
translating.value = false; translating.value = false;
@ -421,10 +419,7 @@ function onContextmenu(ev: MouseEvent): void {
icon: `${icon("ph-arrows-out-simple")}`, icon: `${icon("ph-arrows-out-simple")}`,
text: i18n.ts.showInPage, text: i18n.ts.showInPage,
action: () => { action: () => {
router.push( router.push(notePage(appearNote.value), "forcePage");
notePage(appearNote.value),
"forcePage",
);
}, },
} }
: undefined, : undefined,

View file

@ -310,7 +310,8 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
? instance.defaultReaction ? instance.defaultReaction
: "⭐"; : "⭐";
let readObserver: IntersectionObserver | undefined, connection; let readObserver: IntersectionObserver | undefined;
let connection;
onMounted(() => { onMounted(() => {
if (!props.notification.isRead) { if (!props.notification.isRead) {

View file

@ -84,9 +84,7 @@ function ok() {
} else { } else {
emit("done", { emit("done", {
includingTypes: ( includingTypes: (
Object.keys( Object.keys(typesMap.value) as (typeof notificationTypes)[number][]
typesMap.value,
) as (typeof notificationTypes)[number][]
).filter((type) => typesMap.value[type]), ).filter((type) => typesMap.value[type]),
}); });
} }

View file

@ -71,9 +71,7 @@ const pagination: Paging = {
limit: 10, limit: 10,
params: computed(() => ({ params: computed(() => ({
includeTypes: props.includeTypes ?? undefined, includeTypes: props.includeTypes ?? undefined,
excludeTypes: props.includeTypes excludeTypes: props.includeTypes ? undefined : me.mutingNotificationTypes,
? undefined
: me.mutingNotificationTypes,
unreadOnly: props.unreadOnly, unreadOnly: props.unreadOnly,
})), })),
}; };
@ -114,20 +112,12 @@ onMounted(() => {
connection.on("readNotifications", (notificationIds) => { connection.on("readNotifications", (notificationIds) => {
if (pagingComponent.value) { if (pagingComponent.value) {
for (let i = 0; i < pagingComponent.value.queue.length; i++) { for (let i = 0; i < pagingComponent.value.queue.length; i++) {
if ( if (notificationIds.includes(pagingComponent.value.queue[i].id)) {
notificationIds.includes(pagingComponent.value.queue[i].id)
) {
pagingComponent.value.queue[i].isRead = true; pagingComponent.value.queue[i].isRead = true;
} }
} }
for ( for (let i = 0; i < (pagingComponent.value.items || []).length; i++) {
let i = 0; if (notificationIds.includes(pagingComponent.value.items[i].id)) {
i < (pagingComponent.value.items || []).length;
i++
) {
if (
notificationIds.includes(pagingComponent.value.items[i].id)
) {
pagingComponent.value.items[i].isRead = true; pagingComponent.value.items[i].isRead = true;
} }
} }

View file

@ -164,14 +164,10 @@ const init = async (): Promise<void> => {
res.length > (props.pagination.limit || 10) res.length > (props.pagination.limit || 10)
) { ) {
res.pop(); res.pop();
items.value = props.pagination.reversed items.value = props.pagination.reversed ? [...res].reverse() : res;
? [...res].reverse()
: res;
more.value = true; more.value = true;
} else { } else {
items.value = props.pagination.reversed items.value = props.pagination.reversed ? [...res].reverse() : res;
? [...res].reverse()
: res;
more.value = false; more.value = false;
} }
offset.value = res.length; offset.value = res.length;
@ -381,8 +377,7 @@ const prepend = (item: Item): void => {
const isTop = const isTop =
isBackTop.value || isBackTop.value ||
(document.body.contains(rootEl.value) && (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
isTopVisible(rootEl.value));
if (isTop) { if (isTop) {
// Prepend the item // Prepend the item

View file

@ -74,8 +74,7 @@ const closed = computed(() => remaining.value === 0);
const isLocal = computed(() => !props.note.uri); const isLocal = computed(() => !props.note.uri);
const isVoted = computed( const isVoted = computed(
() => () =>
!props.note.poll.multiple && !props.note.poll.multiple && props.note.poll.choices.some((c) => c.isVoted),
props.note.poll.choices.some((c) => c.isVoted),
); );
const timer = computed(() => const timer = computed(() =>
i18n.t( i18n.t(
@ -101,10 +100,8 @@ const showResult = ref(props.readOnly || isVoted.value);
if (props.note.poll.expiresAt) { if (props.note.poll.expiresAt) {
const tick = () => { const tick = () => {
remaining.value = Math.floor( remaining.value = Math.floor(
Math.max( Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) /
new Date(props.note.poll.expiresAt).getTime() - Date.now(), 1000,
0,
) / 1000,
); );
if (remaining.value === 0) { if (remaining.value === 0) {
showResult.value = true; showResult.value = true;

View file

@ -331,8 +331,8 @@ import { vibrate } from "@/scripts/vibrate";
import { langmap } from "@/scripts/langmap"; import { langmap } from "@/scripts/langmap";
import { import {
detectLanguage, detectLanguage,
isSupportedLang,
isSameLanguage, isSameLanguage,
isSupportedLang,
languageContains, languageContains,
parentLanguage, parentLanguage,
} from "@/scripts/language-utils"; } from "@/scripts/language-utils";
@ -527,9 +527,7 @@ if (
(props.reply.user.host != null && props.reply.user.host !== host)) (props.reply.user.host != null && props.reply.user.host !== host))
) { ) {
text.value = `@${props.reply.user.username}${ text.value = `@${props.reply.user.username}${
props.reply.user.host != null props.reply.user.host != null ? "@" + toASCII(props.reply.user.host) : ""
? "@" + toASCII(props.reply.user.host)
: ""
} `; } `;
} }
@ -589,11 +587,9 @@ if (
} }
if (props.reply.userId !== me.id) { if (props.reply.userId !== me.id) {
os.api("users/show", { userId: props.reply.userId }).then( os.api("users/show", { userId: props.reply.userId }).then((user) => {
(user) => { pushVisibleUser(user);
pushVisibleUser(user); });
},
);
} }
} }
} }
@ -732,9 +728,7 @@ function setVisibility() {
} }
os.popup( os.popup(
defineAsyncComponent( defineAsyncComponent(() => import("@/components/MkVisibilityPicker.vue")),
() => import("@/components/MkVisibilityPicker.vue"),
),
{ {
currentVisibility: visibility.value, currentVisibility: visibility.value,
currentLocalOnly: localOnly.value, currentLocalOnly: localOnly.value,
@ -834,8 +828,7 @@ function setLanguage() {
for (const lang of langs) { for (const lang of langs) {
if (lang === language.value) continue; if (lang === language.value) continue;
if (defaultStore.state.recentlyUsedPostLanguages.includes(lang)) if (defaultStore.state.recentlyUsedPostLanguages.includes(lang)) continue;
continue;
actions.push({ actions.push({
text: langmap[lang].nativeName, text: langmap[lang].nativeName,
danger: false, danger: false,
@ -1072,8 +1065,7 @@ async function post() {
let postData = { let postData = {
editId: props.editId ? props.editId : undefined, editId: props.editId ? props.editId : undefined,
text: processedText === "" ? undefined : processedText, text: processedText === "" ? undefined : processedText,
fileIds: fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined, replyId: props.reply ? props.reply.id : undefined,
renoteId: props.renote renoteId: props.renote
? props.renote.id ? props.renote.id
@ -1085,8 +1077,7 @@ async function post() {
cw: useCw.value ? cw.value || "" : undefined, cw: useCw.value ? cw.value || "" : undefined,
lang: language.value ? language.value : undefined, lang: language.value ? language.value : undefined,
localOnly: localOnly.value, localOnly: localOnly.value,
visibility: visibility: visibility.value === "private" ? "specified" : visibility.value,
visibility.value === "private" ? "specified" : visibility.value,
visibleUserIds: visibleUserIds:
visibility.value === "private" visibility.value === "private"
? [] ? []
@ -1101,9 +1092,7 @@ async function post() {
.split(" ") .split(" ")
.map((x) => (x.startsWith("#") ? x : "#" + x)) .map((x) => (x.startsWith("#") ? x : "#" + x))
.join(" "); .join(" ");
postData.text = postData.text postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
? `${postData.text} ${hashtags_}`
: hashtags_;
} }
// plugin // plugin
@ -1117,9 +1106,7 @@ async function post() {
if (postAccount.value) { if (postAccount.value) {
const storedAccounts = await getAccounts(); const storedAccounts = await getAccounts();
token = storedAccounts.find( token = storedAccounts.find((x) => x.id === postAccount.value.id)?.token;
(x) => x.id === postAccount.value.id,
)?.token;
} }
posting.value = true; posting.value = true;
@ -1165,14 +1152,9 @@ async function post() {
"recentlyUsedPostLanguages", "recentlyUsedPostLanguages",
[language.value] [language.value]
.concat( .concat(
defaultStore.state.recentlyUsedPostLanguages.filter( defaultStore.state.recentlyUsedPostLanguages.filter((lang) => {
(lang) => { return lang !== language.value && languages.includes(lang);
return ( }),
lang !== language.value &&
languages.includes(lang)
);
},
),
) )
.slice(0, maxLength), .slice(0, maxLength),
); );
@ -1268,9 +1250,7 @@ onMounted(() => {
visibility.value = draft.data.visibility; visibility.value = draft.data.visibility;
localOnly.value = draft.data.localOnly; localOnly.value = draft.data.localOnly;
language.value = draft.data.lang; language.value = draft.data.lang;
files.value = (draft.data.files || []).filter( files.value = (draft.data.files || []).filter((draftFile) => draftFile);
(draftFile) => draftFile,
);
if (draft.data.poll) { if (draft.data.poll) {
poll.value = draft.data.poll; poll.value = draft.data.poll;
} }
@ -1298,6 +1278,14 @@ onMounted(() => {
quoteId.value = init.renote ? init.renote.id : null; quoteId.value = init.renote ? init.renote.id : null;
} }
// Inherit language settings when quoting or replying
if (props.renote) {
language.value = props.renote.lang ?? null;
}
if (props.reply) {
language.value = props.reply.lang ?? null;
}
nextTick(() => watchForDraft()); nextTick(() => watchForDraft());
nextTick(() => autosize.update(textareaEl.value)); nextTick(() => autosize.update(textareaEl.value));
}); });

View file

@ -107,8 +107,7 @@ async function describe(file) {
{ {
done: (result) => { done: (result) => {
if (!result || result.canceled) return; if (!result || result.canceled) return;
const comment = const comment = result.result.length === 0 ? null : result.result;
result.result.length === 0 ? null : result.result;
os.api("drive/files/update", { os.api("drive/files/update", {
fileId: file.id, fileId: file.id,
comment, comment,

View file

@ -45,7 +45,7 @@ const isRefreshing = ref(false);
const pullDistance = ref(0); const pullDistance = ref(0);
let disabled = false; let disabled = false;
let supportPointerDesktop = false; const supportPointerDesktop = false;
let startScreenY: number | null = null; let startScreenY: number | null = null;
const rootEl = shallowRef<HTMLDivElement>(); const rootEl = shallowRef<HTMLDivElement>();
@ -102,8 +102,7 @@ function moveBySystem(to: number): Promise<void> {
return; return;
} }
const nextHeight = const nextHeight =
initialHeight - initialHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
(overHeight / RELEASE_TRANSITION_DURATION) * time;
if (pullDistance.value < nextHeight) return; if (pullDistance.value < nextHeight) return;
pullDistance.value = nextHeight; pullDistance.value = nextHeight;
}, 1); }, 1);

View file

@ -94,17 +94,14 @@ const pushRegistrationInServer = ref<
>(); >();
function subscribe() { function subscribe() {
if (!registration.value || !supported.value || !instance.swPublickey) if (!registration.value || !supported.value || !instance.swPublickey) return;
return;
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
return promiseDialog( return promiseDialog(
registration.value.pushManager registration.value.pushManager
.subscribe({ .subscribe({
userVisibleOnly: true, userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array( applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
instance.swPublickey,
),
}) })
.then( .then(
async (subscription) => { async (subscription) => {
@ -121,9 +118,7 @@ function subscribe() {
// When subscribe failed // When subscribe failed
// //
if (err?.name === "NotAllowedError") { if (err?.name === "NotAllowedError") {
console.info( console.info("User denied the notification permission request.");
"User denied the notification permission request.",
);
return; return;
} }
@ -171,9 +166,7 @@ function encode(buffer: ArrayBuffer | null) {
*/ */
function urlBase64ToUint8Array(base64String: string): Uint8Array { function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding) const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
.replace(/-/g, "+")
.replace(/_/g, "/");
const rawData = window.atob(base64); const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length); const outputArray = new Uint8Array(rawData.length);

View file

@ -102,10 +102,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
hasRenotedBefore.value = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
| HTMLElement
| null
| undefined);
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2; const x = rect.left + el.offsetWidth / 2;
@ -129,10 +126,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
hasRenotedBefore.value = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
| HTMLElement
| null
| undefined);
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2; const x = rect.left + el.offsetWidth / 2;
@ -157,10 +151,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
hasRenotedBefore.value = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
| HTMLElement
| null
| undefined);
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2; const x = rect.left + el.offsetWidth / 2;
@ -182,10 +173,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
hasRenotedBefore.value = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
| HTMLElement
| null
| undefined);
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2; const x = rect.left + el.offsetWidth / 2;
@ -221,10 +209,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
hasRenotedBefore.value = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
| HTMLElement
| null
| undefined);
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2; const x = rect.left + el.offsetWidth / 2;

View file

@ -198,9 +198,7 @@ function openDescription(kind: "words" | "users" | "range"): void {
}; };
popup( popup(
defineAsyncComponent( defineAsyncComponent(() => import("@/components/MkSimpleTextWindow.vue")),
() => import("@/components/MkSimpleTextWindow.vue"),
),
{ {
title: i18n.ts.help, title: i18n.ts.help,
description: descriptions[kind], description: descriptions[kind],

View file

@ -205,13 +205,11 @@ function queryKey() {
.get({ .get({
publicKey: { publicKey: {
challenge: byteify(challengeData.value.challenge, "base64"), challenge: byteify(challengeData.value.challenge, "base64"),
allowCredentials: challengeData.value.securityKeys.map( allowCredentials: challengeData.value.securityKeys.map((key) => ({
(key) => ({ id: byteify(key.id, "hex"),
id: byteify(key.id, "hex"), type: "public-key",
type: "public-key", transports: ["usb", "nfc", "ble", "internal"],
transports: ["usb", "nfc", "ble", "internal"], })),
}),
),
timeout: 60 * 1000, timeout: 60 * 1000,
}, },
}) })
@ -226,9 +224,7 @@ function queryKey() {
username: username.value, username: username.value,
password: password.value, password: password.value,
signature: hexify(credential.response.signature), signature: hexify(credential.response.signature),
authenticatorData: hexify( authenticatorData: hexify(credential.response.authenticatorData),
credential.response.authenticatorData,
),
clientDataJSON: hexify(credential.response.clientDataJSON), clientDataJSON: hexify(credential.response.clientDataJSON),
credentialId: credential.id, credentialId: credential.id,
challengeId: challengeData.value.challengeId, challengeId: challengeData.value.challengeId,
@ -277,9 +273,7 @@ function onSubmit() {
"hcaptcha-response": hCaptchaResponse.value, "hcaptcha-response": hCaptchaResponse.value,
"g-recaptcha-response": reCaptchaResponse.value, "g-recaptcha-response": reCaptchaResponse.value,
token: token:
user.value && user.value.twoFactorEnabled user.value && user.value.twoFactorEnabled ? token.value : undefined,
? token.value
: undefined,
}) })
.then((res) => { .then((res) => {
emit("login", res); emit("login", res);

View file

@ -79,8 +79,8 @@ const el = ref<HTMLElement>();
const width = ref(0); const width = ref(0);
const height = ref(0); const height = ref(0);
const colors = ["#eb6f92", "#9ccfd8", "#f6c177", "#f6c177", "#f6c177"]; const colors = ["#eb6f92", "#9ccfd8", "#f6c177", "#f6c177", "#f6c177"];
let stop = false, let stop = false;
ro: ResizeObserver | undefined; let ro: ResizeObserver | undefined;
onMounted(() => { onMounted(() => {
if (!reducedMotion()) { if (!reducedMotion()) {
@ -104,9 +104,7 @@ onMounted(() => {
}; };
particles.value.push(particle); particles.value.push(particle);
window.setTimeout(() => { window.setTimeout(() => {
particles.value = particles.value.filter( particles.value = particles.value.filter((x) => x.id !== particle.id);
(x) => x.id !== particle.id,
);
}, particle.dur - 100); }, particle.dur - 100);
window.setTimeout( window.setTimeout(

View file

@ -60,13 +60,10 @@ function star(ev?: MouseEvent): void {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: props.note.id, noteId: props.note.id,
reaction: reaction:
defaultStore.state.woozyMode === true defaultStore.state.woozyMode === true ? "🥴" : instance.defaultReaction,
? "🥴"
: instance.defaultReaction,
}); });
const el = const el =
ev && ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2; const x = rect.left + el.offsetWidth / 2;

View file

@ -66,8 +66,7 @@ function toggleStar(ev?: MouseEvent): void {
reaction: instance.defaultReaction, reaction: instance.defaultReaction,
}); });
const el = const el =
ev && ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2; const x = rect.left + el.offsetWidth / 2;

View file

@ -32,14 +32,9 @@ export default defineComponent({
role: "tab", role: "tab",
key: option.key, key: option.key,
"aria-selected": "aria-selected":
this.modelValue === option.props?.value this.modelValue === option.props?.value ? "true" : "false",
? "true"
: "false",
onClick: () => { onClick: () => {
this.$emit( this.$emit("update:modelValue", option.props?.value);
"update:modelValue",
option.props?.value,
);
}, },
}, },
option.children, option.children,

View file

@ -25,18 +25,12 @@ const SAFE_FOR_HTML_ID = "abcdefghijklmnopqrstuvwxyz";
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
const idForCanvas = Array.from(Array(16)) const idForCanvas = Array.from(Array(16))
.map( .map(
() => () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)],
SAFE_FOR_HTML_ID[
Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)
],
) )
.join(""); .join("");
const idForTags = Array.from(Array(16)) const idForTags = Array.from(Array(16))
.map( .map(
() => () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)],
SAFE_FOR_HTML_ID[
Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)
],
) )
.join(""); .join("");
const available = ref(false); const available = ref(false);

View file

@ -174,12 +174,12 @@ provide("inWindow", true);
const rootEl = ref<HTMLElement | null>(); const rootEl = ref<HTMLElement | null>();
const showing = ref(true); const showing = ref(true);
let beforeClickedAt = 0, let beforeClickedAt = 0;
maximized = ref(false), const maximized = ref(false);
unMaximizedTop = "", let unMaximizedTop = "";
unMaximizedLeft = "", let unMaximizedLeft = "";
unMaximizedWidth = "", let unMaximizedWidth = "";
unMaximizedHeight = ""; let unMaximizedHeight = "";
function close() { function close() {
showing.value = false; showing.value = false;
@ -203,9 +203,7 @@ function onContextmenu(ev: MouseEvent) {
// //
function top() { function top() {
if (rootEl.value) { if (rootEl.value) {
rootEl.value.style.zIndex = os.claimZIndex( rootEl.value.style.zIndex = os.claimZIndex(props.front ? "middle" : "low");
props.front ? "middle" : "low",
);
} }
} }
@ -282,8 +280,8 @@ function onHeaderMousedown(evt: MouseEvent) {
const windowHeight = main.offsetHeight; const windowHeight = main.offsetHeight;
function move(x: number, y: number) { function move(x: number, y: number) {
let moveLeft = x - moveBaseX, let moveLeft = x - moveBaseX;
moveTop = y - moveBaseY; let moveTop = y - moveBaseY;
// //
if (moveTop + windowHeight > browserHeight) if (moveTop + windowHeight > browserHeight)
@ -310,13 +308,9 @@ function onHeaderMousedown(evt: MouseEvent) {
// //
dragListen((me) => { dragListen((me) => {
const x = const x =
me.touches && me.touches.length > 0 me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
? me.touches[0].clientX
: me.clientX;
const y = const y =
me.touches && me.touches.length > 0 me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
? me.touches[0].clientY
: me.clientY;
move(x, y); move(x, y);
}); });

View file

@ -146,14 +146,12 @@ useInterval(
() => { () => {
if (prefixEl.value) { if (prefixEl.value) {
if (prefixEl.value.offsetWidth) { if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + "px";
prefixEl.value.offsetWidth + "px";
} }
} }
if (suffixEl.value) { if (suffixEl.value) {
if (suffixEl.value.offsetWidth) { if (suffixEl.value.offsetWidth) {
inputEl.value.style.paddingRight = inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + "px";
suffixEl.value.offsetWidth + "px";
} }
} }
}, },

View file

@ -60,8 +60,7 @@ export default defineComponent({
value: option.props?.value, value: option.props?.value,
disabled: option.props?.disabled, disabled: option.props?.disabled,
modelValue: this.value, modelValue: this.value,
"onUpdate:modelValue": (value) => "onUpdate:modelValue": (value) => (this.value = value),
(this.value = value),
}, },
option.children, option.children,
), ),

View file

@ -120,14 +120,12 @@ useInterval(
() => { () => {
if (prefixEl.value) { if (prefixEl.value) {
if (prefixEl.value.offsetWidth) { if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + "px";
prefixEl.value.offsetWidth + "px";
} }
} }
if (suffixEl.value) { if (suffixEl.value) {
if (suffixEl.value.offsetWidth) { if (suffixEl.value.offsetWidth) {
inputEl.value.style.paddingRight = inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + "px";
suffixEl.value.offsetWidth + "px";
} }
} }
}, },

View file

@ -88,9 +88,7 @@ const choseAd = (): Ad | null => {
return widgetAds; return widgetAds;
} else if (ads.length === 0) { } else if (ads.length === 0) {
if (lowPriorityAds.length !== 0) { if (lowPriorityAds.length !== 0) {
return lowPriorityAds[ return lowPriorityAds[Math.floor(Math.random() * lowPriorityAds.length)];
Math.floor(Math.random() * lowPriorityAds.length)
];
} else { } else {
return null; return null;
} }

View file

@ -24,10 +24,10 @@ const props = withDefaults(
}, },
); );
let ro: ResizeObserver, let ro: ResizeObserver;
root = ref<HTMLElement>(), const root = ref<HTMLElement>();
content = ref<HTMLElement>(), const content = ref<HTMLElement>();
margin = ref(0); const margin = ref(0);
const shouldSpacerMin = inject("shouldSpacerMin", false); const shouldSpacerMin = inject("shouldSpacerMin", false);
const adjust = (rect: { width: number; height: number }) => { const adjust = (rect: { width: number; height: number }) => {

View file

@ -51,9 +51,7 @@ const el = ref();
useTooltip(el, (showing) => { useTooltip(el, (showing) => {
os.popup( os.popup(
defineAsyncComponent( defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
() => import("@/components/MkUrlPreviewPopup.vue"),
),
{ {
showing, showing,
url: props.url, url: props.url,

View file

@ -36,11 +36,9 @@ export default defineComponent({
const note: Ref<Record<string, any> | null> = ref(null); const note: Ref<Record<string, any> | null> = ref(null);
onMounted(() => { onMounted(() => {
os.api("notes/show", { noteId: props.block.note }).then( os.api("notes/show", { noteId: props.block.note }).then((result) => {
(result) => { note.value = result;
note.value = result; });
},
);
}); });
return { return {

View file

@ -63,10 +63,7 @@ export default defineComponent({
const formData = new FormData(); const formData = new FormData();
formData.append("file", blob); formData.append("file", blob);
if (this.defaultStore.state.uploadFolder) { if (this.defaultStore.state.uploadFolder) {
formData.append( formData.append("folderId", this.defaultStore.state.uploadFolder);
"folderId",
this.defaultStore.state.uploadFolder,
);
} }
fetch(apiUrl + "/drive/files/create", { fetch(apiUrl + "/drive/files/create", {
@ -87,9 +84,7 @@ export default defineComponent({
}, },
async post() { async post() {
this.posting = true; this.posting = true;
const file = this.block.attachCanvasImage const file = this.block.attachCanvasImage ? await this.upload() : null;
? await this.upload()
: null;
os.apiWithDialog("notes/create", { os.apiWithDialog("notes/create", {
text: this.text === "" ? null : this.text, text: this.text === "" ? null : this.text,
fileIds: file ? [file.id] : undefined, fileIds: file ? [file.id] : undefined,

View file

@ -93,8 +93,8 @@ export class Router extends EventEmitter<{
} }
public resolve(path: string): Resolved | null { public resolve(path: string): Resolved | null {
let queryString: string | null = null, let queryString: string | null = null;
hash: string | null = null; let hash: string | null = null;
if (path[0] === "/") path = path.substring(1); if (path[0] === "/") path = path.substring(1);
if (path.includes("#")) { if (path.includes("#")) {
hash = path.substring(path.indexOf("#") + 1); hash = path.substring(path.indexOf("#") + 1);

View file

@ -776,8 +776,8 @@ type AwaitType<T> = T extends Promise<infer U>
: T extends (...args: any[]) => Promise<infer V> : T extends (...args: any[]) => Promise<infer V>
? V ? V
: T; : T;
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null, let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null; let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
export async function openEmojiPicker( export async function openEmojiPicker(
src?: HTMLElement, src?: HTMLElement,
opts, opts,

View file

@ -94,10 +94,7 @@ export default defineComponent({
methods: { methods: {
search() { search() {
if ( if ((this.q === "" || this.q == null) && this.selectedTags.size === 0) {
(this.q === "" || this.q == null) &&
this.selectedTags.size === 0
) {
this.searchEmojis = null; this.searchEmojis = null;
return; return;
} }
@ -105,17 +102,13 @@ export default defineComponent({
if (this.selectedTags.size === 0) { if (this.selectedTags.size === 0) {
this.searchEmojis = this.customEmojis.filter( this.searchEmojis = this.customEmojis.filter(
(emoji) => (emoji) =>
emoji.name.includes(this.q) || emoji.name.includes(this.q) || emoji.aliases.includes(this.q),
emoji.aliases.includes(this.q),
); );
} else { } else {
this.searchEmojis = this.customEmojis.filter( this.searchEmojis = this.customEmojis.filter(
(emoji) => (emoji) =>
(emoji.name.includes(this.q) || (emoji.name.includes(this.q) || emoji.aliases.includes(this.q)) &&
emoji.aliases.includes(this.q)) && [...this.selectedTags].every((t) => emoji.aliases.includes(t)),
[...this.selectedTags].every((t) =>
emoji.aliases.includes(t),
),
); );
} }
}, },

View file

@ -276,9 +276,7 @@ function easterEgg() {
setTimeout(() => { setTimeout(() => {
if (iconClicks % 6 === 0) { if (iconClicks % 6 === 0) {
iconSrc.value = iconSrc.value =
instance.iconUrl || instance.iconUrl || instance.faviconUrl || "/favicon.ico";
instance.faviconUrl ||
"/favicon.ico";
} else { } else {
iconSrc.value = "/static-assets/woozy.png"; iconSrc.value = "/static-assets/woozy.png";
} }

View file

@ -254,9 +254,7 @@ const headerTabs = computed(() => [
definePageMetadata( definePageMetadata(
computed(() => ({ computed(() => ({
title: file.value title: file.value ? i18n.ts.file + ": " + file.value.name : i18n.ts.file,
? i18n.ts.file + ": " + file.value.name
: i18n.ts.file,
icon: `${icon("ph-file")}`, icon: `${icon("ph-file")}`,
})), })),
); );

View file

@ -169,12 +169,10 @@ onMounted(() => {
if (tabEl && tabHighlightEl.value) { if (tabEl && tabHighlightEl.value) {
// offsetWidth offsetLeft getBoundingClientRect 使 // offsetWidth offsetLeft getBoundingClientRect 使
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 // https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
const parentRect = const parentRect = tabEl.parentElement.getBoundingClientRect();
tabEl.parentElement.getBoundingClientRect();
const rect = tabEl.getBoundingClientRect(); const rect = tabEl.getBoundingClientRect();
tabHighlightEl.value.style.width = rect.width + "px"; tabHighlightEl.value.style.width = rect.width + "px";
tabHighlightEl.value.style.left = tabHighlightEl.value.style.left = rect.left - parentRect.left + "px";
rect.left - parentRect.left + "px";
} }
}); });
}, },

View file

@ -97,9 +97,7 @@ function remove(announcement) {
text: i18n.t("removeAreYouSure", { x: announcement.title }), text: i18n.t("removeAreYouSure", { x: announcement.title }),
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) return; if (canceled) return;
announcements.value = announcements.value.filter( announcements.value = announcements.value.filter((x) => x !== announcement);
(x) => x !== announcement,
);
os.api("admin/announcements/delete", announcement); os.api("admin/announcements/delete", announcement);
}); });
} }

View file

@ -46,9 +46,7 @@ import icon from "@/scripts/icon";
const databasePromiseFactory = () => const databasePromiseFactory = () =>
os os
.api("admin/get-table-stats") .api("admin/get-table-stats")
.then((res) => .then((res) => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
Object.entries(res).sort((a, b) => b[1].size - a[1].size),
);
const headerActions = computed(() => []); const headerActions = computed(() => []);

View file

@ -184,9 +184,7 @@ const remotePagination = {
limit: 30, limit: 30,
params: computed(() => ({ params: computed(() => ({
query: query:
queryRemote.value && queryRemote.value !== "" queryRemote.value && queryRemote.value !== "" ? queryRemote.value : null,
? queryRemote.value
: null,
host: host.value && host.value !== "" ? host.value : null, host: host.value && host.value !== "" ? host.value : null,
})), })),
}; };
@ -203,9 +201,7 @@ const selectAll = () => {
const toggleSelect = (emoji) => { const toggleSelect = (emoji) => {
if (selectedEmojis.value.includes(emoji.id)) { if (selectedEmojis.value.includes(emoji.id)) {
selectedEmojis.value = selectedEmojis.value.filter( selectedEmojis.value = selectedEmojis.value.filter((x) => x !== emoji.id);
(x) => x !== emoji.id,
);
} else { } else {
selectedEmojis.value.push(emoji.id); selectedEmojis.value.push(emoji.id);
} }
@ -305,9 +301,7 @@ const menu = (ev: MouseEvent) => {
icon: `${icon("ph-upload-simple")}`, icon: `${icon("ph-upload-simple")}`,
text: i18n.ts.importZip, text: i18n.ts.importZip,
action: async () => { action: async () => {
const file = await selectFile( const file = await selectFile(ev.currentTarget ?? ev.target);
ev.currentTarget ?? ev.target,
);
os.api("admin/emoji/import-zip", { os.api("admin/emoji/import-zip", {
fileId: file.id, fileId: file.id,
}) })
@ -329,10 +323,7 @@ const menu = (ev: MouseEvent) => {
icon: `${icon("ph-info")}`, icon: `${icon("ph-info")}`,
text: i18n.ts.emojiPackCreator, text: i18n.ts.emojiPackCreator,
action: () => { action: () => {
window.open( window.open("https://firefish.dev/firefish/emoji-gen", "_blank");
"https://firefish.dev/firefish/emoji-gen",
"_blank",
);
}, },
}, },
], ],

View file

@ -103,9 +103,7 @@ const pagination = {
userId: userId.value && userId.value !== "" ? userId.value : null, userId: userId.value && userId.value !== "" ? userId.value : null,
origin: origin.value, origin: origin.value,
hostname: hostname:
searchHost.value && searchHost.value !== "" searchHost.value && searchHost.value !== "" ? searchHost.value : null,
? searchHost.value
: null,
})), })),
}; };

View file

@ -46,8 +46,7 @@ async function init() {
function save() { function save() {
os.apiWithDialog("admin/update-meta", { os.apiWithDialog("admin/update-meta", {
hiddenTags: hiddenTags: hiddenTags.value.split("\n").map((h: string) => h.trim()) || [],
hiddenTags.value.split("\n").map((h: string) => h.trim()) || [],
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();
}); });

View file

@ -221,31 +221,25 @@ const menuDef = computed(() => [
icon: `${icon("ph-gear-six")}`, icon: `${icon("ph-gear-six")}`,
text: i18n.ts.general, text: i18n.ts.general,
to: "/admin/settings", to: "/admin/settings",
active: active: currentPage.value?.route.name === "settings",
currentPage.value?.route.name === "settings",
}, },
{ {
icon: `${icon("ph-envelope-simple-open")}`, icon: `${icon("ph-envelope-simple-open")}`,
text: i18n.ts.emailServer, text: i18n.ts.emailServer,
to: "/admin/email-settings", to: "/admin/email-settings",
active: active: currentPage.value?.route.name === "email-settings",
currentPage.value?.route.name ===
"email-settings",
}, },
{ {
icon: `${icon("ph-cloud")}`, icon: `${icon("ph-cloud")}`,
text: i18n.ts.objectStorage, text: i18n.ts.objectStorage,
to: "/admin/object-storage", to: "/admin/object-storage",
active: active: currentPage.value?.route.name === "object-storage",
currentPage.value?.route.name ===
"object-storage",
}, },
{ {
icon: `${icon("ph-lock")}`, icon: `${icon("ph-lock")}`,
text: i18n.ts.security, text: i18n.ts.security,
to: "/admin/security", to: "/admin/security",
active: active: currentPage.value?.route.name === "security",
currentPage.value?.route.name === "security",
}, },
{ {
icon: `${icon("ph-arrows-merge")}`, icon: `${icon("ph-arrows-merge")}`,
@ -257,38 +251,31 @@ const menuDef = computed(() => [
icon: `${icon("ph-prohibit")}`, icon: `${icon("ph-prohibit")}`,
text: i18n.ts.instanceBlocking, text: i18n.ts.instanceBlocking,
to: "/admin/instance-block", to: "/admin/instance-block",
active: active: currentPage.value?.route.name === "instance-block",
currentPage.value?.route.name ===
"instance-block",
}, },
{ {
icon: `${icon("ph-hash")}`, icon: `${icon("ph-hash")}`,
text: i18n.ts.hiddenTags, text: i18n.ts.hiddenTags,
to: "/admin/hashtags", to: "/admin/hashtags",
active: active: currentPage.value?.route.name === "hashtags",
currentPage.value?.route.name === "hashtags",
}, },
{ {
icon: `${icon("ph-ghost")}`, icon: `${icon("ph-ghost")}`,
text: i18n.ts.proxyAccount, text: i18n.ts.proxyAccount,
to: "/admin/proxy-account", to: "/admin/proxy-account",
active: active: currentPage.value?.route.name === "proxy-account",
currentPage.value?.route.name ===
"proxy-account",
}, },
{ {
icon: `${icon("ph-database")}`, icon: `${icon("ph-database")}`,
text: i18n.ts.database, text: i18n.ts.database,
to: "/admin/database", to: "/admin/database",
active: active: currentPage.value?.route.name === "database",
currentPage.value?.route.name === "database",
}, },
{ {
icon: `${icon("ph-flask")}`, icon: `${icon("ph-flask")}`,
text: i18n.ts._experiments.title, text: i18n.ts._experiments.title,
to: "/admin/experiments", to: "/admin/experiments",
active: active: currentPage.value?.route.name === "experiments",
currentPage.value?.route.name === "experiments",
}, },
], ],
}, },

View file

@ -70,8 +70,7 @@ async function init() {
function save() { function save() {
os.apiWithDialog("admin/update-meta", { os.apiWithDialog("admin/update-meta", {
blockedHosts: blockedHosts.value.split("\n").map((h) => h.trim()) || [], blockedHosts: blockedHosts.value.split("\n").map((h) => h.trim()) || [],
silencedHosts: silencedHosts: silencedHosts.value.split("\n").map((h) => h.trim()) || [],
silencedHosts.value.split("\n").map((h) => h.trim()) || [],
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();
}); });

View file

@ -530,7 +530,8 @@ function isValidHttpUrl(src: string) {
function parseMoreUrls(src: string): { name: string; url: string }[] { function parseMoreUrls(src: string): { name: string; url: string }[] {
const toReturn: { name: string; url: string }[] = []; const toReturn: { name: string; url: string }[] = [];
const pattern = /"(.+)"\s*:\s*(http.+)/; const pattern = /"(.+)"\s*:\s*(http.+)/;
src.trim() src
.trim()
.split("\n") .split("\n")
.forEach((line) => { .forEach((line) => {
const match = pattern.exec(line); const match = pattern.exec(line);
@ -590,9 +591,7 @@ async function init() {
defaultReaction.value = ["⭐", "👍", "❤️"].includes(meta.defaultReaction) defaultReaction.value = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
? meta.defaultReaction ? meta.defaultReaction
: "custom"; : "custom";
defaultReactionCustom.value = ["⭐", "👍", "❤️"].includes( defaultReactionCustom.value = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
meta.defaultReaction,
)
? "" ? ""
: meta.defaultReaction; : meta.defaultReaction;
enableServerMachineStats.value = meta.enableServerMachineStats; enableServerMachineStats.value = meta.enableServerMachineStats;

View file

@ -72,11 +72,9 @@ onMounted(() => {
fetching.value = false; fetching.value = false;
if (session.value.app.isAuthorized) { if (session.value.app.isAuthorized) {
os.api("auth/accept", { token: session.value.token }).then( os.api("auth/accept", { token: session.value.token }).then(() => {
() => { accepted();
accepted(); });
},
);
} else { } else {
state.value = "waiting"; state.value = "waiting";
} }

View file

@ -4,11 +4,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, onMounted, ref } from "vue";
import * as os from "@/os"; import * as os from "@/os";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
import { onMounted, ref, defineAsyncComponent } from "vue";
const XNotFound = defineAsyncComponent(() => import("./not-found.vue")); const XNotFound = defineAsyncComponent(() => import("./not-found.vue"));
const err = ref(false); const err = ref(false);

View file

@ -75,28 +75,25 @@ const headerActions = computed(() =>
icon: `${icon("ph-pencil")}`, icon: `${icon("ph-pencil")}`,
text: i18n.ts.toEdit, text: i18n.ts.toEdit,
handler: async (): Promise<void> => { handler: async (): Promise<void> => {
const { canceled, result } = await os.form( const { canceled, result } = await os.form(clip.value.name, {
clip.value.name, name: {
{ type: "string",
name: { label: i18n.ts.name,
type: "string", default: clip.value.name,
label: i18n.ts.name,
default: clip.value.name,
},
description: {
type: "string",
required: false,
multiline: true,
label: i18n.ts.description,
default: clip.value.description,
},
isPublic: {
type: "boolean",
label: i18n.ts.public,
default: clip.value.isPublic,
},
}, },
); description: {
type: "string",
required: false,
multiline: true,
label: i18n.ts.description,
default: clip.value.description,
},
isPublic: {
type: "boolean",
label: i18n.ts.public,
default: clip.value.isPublic,
},
});
if (canceled) return; if (canceled) return;
os.apiWithDialog("clips/update", { os.apiWithDialog("clips/update", {

View file

@ -37,14 +37,12 @@ function menu(ev) {
text: i18n.ts.license, text: i18n.ts.license,
icon: `${icon("ph-info")}`, icon: `${icon("ph-info")}`,
action: () => { action: () => {
os.apiGet("emoji", { name: props.emoji.name }).then( os.apiGet("emoji", { name: props.emoji.name }).then((res) => {
(res) => { os.alert({
os.alert({ type: "info",
type: "info", text: `${res.license || i18n.ts.notSet}`,
text: `${res.license || i18n.ts.notSet}`, });
}); });
},
);
}, },
}, },
], ],

View file

@ -182,8 +182,7 @@ function onMessage(message): void {
!( !(
(m.recipientId === message.recipientId && (m.recipientId === message.recipientId &&
m.userId === message.userId) || m.userId === message.userId) ||
(m.recipientId === message.userId && (m.recipientId === message.userId && m.userId === message.recipientId)
m.userId === message.recipientId)
), ),
); );
@ -267,8 +266,7 @@ onMounted(() => {
const _messages = userMessages.concat(groupMessages); const _messages = userMessages.concat(groupMessages);
_messages.sort( _messages.sort(
(a, b) => (a, b) =>
new Date(b.createdAt).getTime() - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
new Date(a.createdAt).getTime(),
); );
messages.value = _messages; messages.value = _messages;
}, },

View file

@ -90,8 +90,7 @@ const draftKey = computed(() =>
props.user ? "user:" + props.user.id : "group:" + props.group?.id, props.user ? "user:" + props.user.id : "group:" + props.group?.id,
); );
const canSend = computed( const canSend = computed(
() => () => (text.value != null && text.value.trim() !== "") || file.value != null,
(text.value != null && text.value.trim() !== "") || file.value != null,
); );
watch([text, file], saveDraft); watch([text, file], saveDraft);

View file

@ -280,9 +280,7 @@ function onRead(x) {
if (!Array.isArray(x)) x = [x]; if (!Array.isArray(x)) x = [x];
for (const id of x) { for (const id of x) {
if (pagingComponent.value.items.some((y) => y.id === id)) { if (pagingComponent.value.items.some((y) => y.id === id)) {
const exist = pagingComponent.value.items const exist = pagingComponent.value.items.map((y) => y.id).indexOf(id);
.map((y) => y.id)
.indexOf(id);
pagingComponent.value.items[exist] = { pagingComponent.value.items[exist] = {
...pagingComponent.value.items[exist], ...pagingComponent.value.items[exist],
isRead: true, isRead: true,
@ -292,15 +290,10 @@ function onRead(x) {
} else if (group.value) { } else if (group.value) {
for (const id of x.ids) { for (const id of x.ids) {
if (pagingComponent.value.items.some((y) => y.id === id)) { if (pagingComponent.value.items.some((y) => y.id === id)) {
const exist = pagingComponent.value.items const exist = pagingComponent.value.items.map((y) => y.id).indexOf(id);
.map((y) => y.id)
.indexOf(id);
pagingComponent.value.items[exist] = { pagingComponent.value.items[exist] = {
...pagingComponent.value.items[exist], ...pagingComponent.value.items[exist],
reads: [ reads: [...pagingComponent.value.items[exist].reads, x.userId],
...pagingComponent.value.items[exist].reads,
x.userId,
],
}; };
} }
} }

View file

@ -72,8 +72,8 @@ const headerTabs = computed(() => []);
const list = ref<typeof MkPagination | null>(null); const list = ref<typeof MkPagination | null>(null);
let isCached = false, let isCached = false;
refreshTimer: number | null = null; let refreshTimer: number | null = null;
const refresh = () => { const refresh = () => {
if (isCached) { if (isCached) {

View file

@ -175,20 +175,15 @@ definePageMetadata(
appearNote.value appearNote.value
? { ? {
title: i18n.t("noteOf", { title: i18n.t("noteOf", {
user: user: appearNote.value.user.name || appearNote.value.user.username,
appearNote.value.user.name ||
appearNote.value.user.username,
}), }),
subtitle: new Date( subtitle: new Date(appearNote.value.createdAt).toLocaleString(),
appearNote.value.createdAt,
).toLocaleString(),
avatar: appearNote.value.user, avatar: appearNote.value.user,
path: `/notes/${appearNote.value.id}`, path: `/notes/${appearNote.value.id}`,
share: { share: {
title: i18n.t("noteOf", { title: i18n.t("noteOf", {
user: user:
appearNote.value.user.name || appearNote.value.user.name || appearNote.value.user.username,
appearNote.value.user.username,
}), }),
text: appearNote.value.text, text: appearNote.value.text,
}, },

View file

@ -164,9 +164,7 @@ export default defineComponent({
components: { components: {
XContainer, XContainer,
MkTextarea, MkTextarea,
XV: defineAsyncComponent( XV: defineAsyncComponent(() => import("./page-editor.script-block.vue")),
() => import("./page-editor.script-block.vue"),
),
}, },
inject: ["getScriptBlockList"], inject: ["getScriptBlockList"],
@ -228,12 +226,10 @@ export default defineComponent({
watch: { watch: {
slots: { slots: {
handler() { handler() {
this.modelValue.value.slots = this.slots this.modelValue.value.slots = this.slots.split("\n").map((x) => ({
.split("\n") name: x,
.map((x) => ({ type: null,
name: x, }));
type: null,
}));
}, },
deep: true, deep: true,
}, },
@ -243,9 +239,7 @@ export default defineComponent({
if (this.modelValue.value == null) this.modelValue.value = null; if (this.modelValue.value == null) this.modelValue.value = null;
if (this.modelValue.value && this.modelValue.value.slots) if (this.modelValue.value && this.modelValue.value.slots)
this.slots = this.modelValue.value.slots this.slots = this.modelValue.value.slots.map((x) => x.name).join("\n");
.map((x) => x.name)
.join("\n");
this.$watch( this.$watch(
() => this.modelValue.type, () => this.modelValue.type,
@ -261,10 +255,7 @@ export default defineComponent({
return; return;
} }
if ( if (this.modelValue.type && this.modelValue.type.startsWith("fn:")) {
this.modelValue.type &&
this.modelValue.type.startsWith("fn:")
) {
const fnName = this.modelValue.type.split(":")[1]; const fnName = this.modelValue.type.split(":")[1];
const fn = this.hpml.getVarByName(fnName); const fn = this.hpml.getVarByName(fnName);
@ -280,27 +271,17 @@ export default defineComponent({
if (isLiteralValue(this.modelValue)) return; if (isLiteralValue(this.modelValue)) return;
const empties = []; const empties = [];
for ( for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
let i = 0;
i < funcDefs[this.modelValue.type].in.length;
i++
) {
const id = uuid(); const id = uuid();
empties.push({ id, type: null }); empties.push({ id, type: null });
} }
this.modelValue.args = empties; this.modelValue.args = empties;
for ( for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
let i = 0;
i < funcDefs[this.modelValue.type].in.length;
i++
) {
const inType = funcDefs[this.modelValue.type].in[i]; const inType = funcDefs[this.modelValue.type].in[i];
if (typeof inType !== "number") { if (typeof inType !== "number") {
if (inType === "number") if (inType === "number") this.modelValue.args[i].type = "number";
this.modelValue.args[i].type = "number"; if (inType === "string") this.modelValue.args[i].type = "text";
if (inType === "string")
this.modelValue.args[i].type = "text";
} }
} }
}, },

View file

@ -47,7 +47,6 @@
v-tooltip="i18n.ts._pages.viewSource" v-tooltip="i18n.ts._pages.viewSource"
:to="`/@${username}/pages/${pageName}/view-source`" :to="`/@${username}/pages/${pageName}/view-source`"
class="menu _button" class="menu _button"
style="transform: translateY(2px)"
><i :class="icon('ph-code')" ><i :class="icon('ph-code')"
/></MkA> /></MkA>
<template <template
@ -59,7 +58,6 @@
v-tooltip="i18n.ts._pages.editPage" v-tooltip="i18n.ts._pages.editPage"
class="menu _button" class="menu _button"
:to="`/pages/edit/${page.id}`" :to="`/pages/edit/${page.id}`"
style="transform: translateY(2px)"
><i :class="icon('ph-pencil')" ><i :class="icon('ph-pencil')"
/></MkA> /></MkA>
<button <button
@ -215,7 +213,7 @@ import { definePageMetadata } from "@/scripts/page-metadata";
import { shareAvailable } from "@/scripts/share-available"; import { shareAvailable } from "@/scripts/share-available";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import { isSignedIn } from "@/me"; import { me, isSignedIn } from "@/me";
const props = defineProps<{ const props = defineProps<{
pageName: string; pageName: string;
@ -381,6 +379,7 @@ definePageMetadata(
color: #fff; color: #fff;
text-shadow: 0 0 8px var(--shadow); text-shadow: 0 0 8px var(--shadow);
font-size: 16px; font-size: 16px;
display: inline-flex;
} }
> .koudoku { > .koudoku {

View file

@ -64,9 +64,7 @@ function fetchKeys() {
os.api("i/registry/keys-with-type", { os.api("i/registry/keys-with-type", {
scope: scope.value, scope: scope.value,
}).then((res) => { }).then((res) => {
keys.value = Object.entries(res).sort((a, b) => keys.value = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
a[0].localeCompare(b[0]),
);
}); });
} }

View file

@ -56,9 +56,7 @@ async function save() {
onMounted(() => { onMounted(() => {
if (props.token == null) { if (props.token == null) {
os.popup( os.popup(
defineAsyncComponent( defineAsyncComponent(() => import("@/components/MkForgotPassword.vue")),
() => import("@/components/MkForgotPassword.vue"),
),
{}, {},
{}, {},
"closed", "closed",

View file

@ -89,10 +89,7 @@ async function run() {
out: (value) => { out: (value) => {
logs.value.push({ logs.value.push({
id: Math.random(), id: Math.random(),
text: text: value.type === "str" ? value.value : utils.valToString(value),
value.type === "str"
? value.value
: utils.valToString(value),
print: true, print: true,
}); });
}, },

View file

@ -78,8 +78,7 @@ const notesPagination = {
host: props.host == null ? undefined : getHost(props.host), host: props.host == null ? undefined : getHost(props.host),
sinceDate: sinceDate:
props.since == null ? undefined : getUnixTime(props.since, false), props.since == null ? undefined : getUnixTime(props.since, false),
untilDate: untilDate: props.until == null ? undefined : getUnixTime(props.until, true),
props.until == null ? undefined : getUnixTime(props.until, true),
withFiles: props.withFiles === "1", withFiles: props.withFiles === "1",
searchCwAndAlt: props.searchCwAndAlt === "1", searchCwAndAlt: props.searchCwAndAlt === "1",
channelId: props.channel, channelId: props.channel,

View file

@ -107,8 +107,7 @@ async function install() {
return; return;
} }
const { name, version, author, description, permissions, config } = const { name, version, author, description, permissions, config } = metadata;
metadata;
if (name == null || version == null || author == null) { if (name == null || version == null || author == null) {
os.alert({ os.alert({
type: "error", type: "error",
@ -123,29 +122,22 @@ async function install() {
: await new Promise((res, rej) => { : await new Promise((res, rej) => {
os.popup( os.popup(
defineAsyncComponent( defineAsyncComponent(
() => () => import("@/components/MkTokenGenerateWindow.vue"),
import(
"@/components/MkTokenGenerateWindow.vue"
),
), ),
{ {
title: i18n.ts.tokenRequested, title: i18n.ts.tokenRequested,
information: information: i18n.ts.pluginTokenRequestedDescription,
i18n.ts.pluginTokenRequestedDescription,
initialName: name, initialName: name,
initialPermissions: permissions, initialPermissions: permissions,
}, },
{ {
done: async (result) => { done: async (result) => {
const { name, permissions } = result; const { name, permissions } = result;
const { token } = await os.api( const { token } = await os.api("miauth/gen-token", {
"miauth/gen-token", session: null,
{ name,
session: null, permission: permissions,
name, });
permission: permissions,
},
);
res(token); res(token);
}, },
}, },

View file

@ -196,10 +196,7 @@ function validate(profile: unknown): void {
// Check if createdAt and updatedAt is Date // Check if createdAt and updatedAt is Date
// https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date // https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date
if ( if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt).getTime()))
!profile.createdAt ||
Number.isNaN(new Date(profile.createdAt).getTime())
)
throw new Error("createdAt is falsy or not Date"); throw new Error("createdAt is falsy or not Date");
if (profile.updatedAt) { if (profile.updatedAt) {
if (Number.isNaN(new Date(profile.updatedAt).getTime())) { if (Number.isNaN(new Date(profile.updatedAt).getTime())) {
@ -228,10 +225,7 @@ function getSettings(): Profile["settings"] {
hot, hot,
cold, cold,
fontSize: localStorage.getItem("fontSize"), fontSize: localStorage.getItem("fontSize"),
useSystemFont: localStorage.getItem("useSystemFont") as useSystemFont: localStorage.getItem("useSystemFont") as "t" | "f" | null,
| "t"
| "f"
| null,
wallpaper: localStorage.getItem("wallpaper"), wallpaper: localStorage.getItem("wallpaper"),
}; };
} }
@ -468,12 +462,9 @@ function menu(ev: MouseEvent, profileId: string) {
text: i18n.ts.download, text: i18n.ts.download,
icon: `${icon("ph-download-simple")}`, icon: `${icon("ph-download-simple")}`,
href: URL.createObjectURL( href: URL.createObjectURL(
new Blob( new Blob([JSON.stringify(profiles.value[profileId], null, 2)], {
[JSON.stringify(profiles.value[profileId], null, 2)], type: "application/json",
{ }),
type: "application/json",
},
),
), ),
download: `${profiles.value[profileId].name}.json`, download: `${profiles.value[profileId].name}.json`,
}, },
@ -502,20 +493,17 @@ function menu(ev: MouseEvent, profileId: string) {
onMounted(() => { onMounted(() => {
// streaminguser storage update // streaminguser storage update
connection?.on( connection?.on("registryUpdated", ({ scope: recievedScope, key, value }) => {
"registryUpdated", if (
({ scope: recievedScope, key, value }) => { !recievedScope ||
if ( recievedScope.length !== scope.length ||
!recievedScope || recievedScope[0] !== scope[0]
recievedScope.length !== scope.length || )
recievedScope[0] !== scope[0] return;
) if (!profiles.value) return;
return;
if (!profiles.value) return;
profiles.value[key] = value; profiles.value[key] = value;
}, });
);
}); });
onUnmounted(() => { onUnmounted(() => {

View file

@ -213,9 +213,7 @@ while (fields.length < 4) {
function saveFields() { function saveFields() {
os.apiWithDialog("i/update", { os.apiWithDialog("i/update", {
fields: fields.filter( fields: fields.filter((field) => field.name !== "" && field.value !== ""),
(field) => field.name !== "" && field.value !== "",
),
}); });
} }

View file

@ -179,9 +179,7 @@ function remove(reaction, ev: MouseEvent) {
{ {
text: i18n.ts.remove, text: i18n.ts.remove,
action: () => { action: () => {
reactions.value = reactions.value.filter( reactions.value = reactions.value.filter((x) => x !== reaction);
(x) => x !== reaction,
);
save(); save();
}, },
}, },
@ -192,9 +190,7 @@ function remove(reaction, ev: MouseEvent) {
function preview(ev: MouseEvent) { function preview(ev: MouseEvent) {
os.popup( os.popup(
defineAsyncComponent( defineAsyncComponent(() => import("@/components/MkEmojiPickerDialog.vue")),
() => import("@/components/MkEmojiPickerDialog.vue"),
),
{ {
asReactionPicker: true, asReactionPicker: true,
src: ev.currentTarget ?? ev.target, src: ev.currentTarget ?? ev.target,

View file

@ -71,13 +71,11 @@ const pagination = {
}; };
async function change() { async function change() {
const { canceled: canceled1, result: currentPassword } = await os.inputText( const { canceled: canceled1, result: currentPassword } = await os.inputText({
{ title: i18n.ts.currentPassword,
title: i18n.ts.currentPassword, type: "password",
type: "password", autocomplete: "current-password",
autocomplete: "current-password", });
},
);
if (canceled1) return; if (canceled1) return;
const { canceled: canceled2, result: newPassword } = await os.inputText({ const { canceled: canceled2, result: newPassword } = await os.inputText({

Some files were not shown because too many files have changed in this diff Show more