Merge branch 'develop' of https://firefish.dev/firefish/firefish into feat/note-edit-history
This commit is contained in:
commit
e8f178458f
133 changed files with 720 additions and 895 deletions
8
Makefile
8
Makefile
|
@ -1,7 +1,5 @@
|
|||
ifneq (dev,$(wildcard config.env))
|
||||
include ./dev/config.env
|
||||
export
|
||||
endif
|
||||
|
||||
|
||||
.PHONY: pre-commit
|
||||
|
@ -29,13 +27,13 @@ build:
|
|||
pnpm run migrate
|
||||
|
||||
|
||||
.PHONY: db.init db.up db.down
|
||||
db.init:
|
||||
$(MAKE) -C ./dev/db-container init
|
||||
.PHONY: db.up db.down db.init
|
||||
db.up:
|
||||
$(MAKE) -C ./dev/db-container up
|
||||
db.down:
|
||||
$(MAKE) -C ./dev/db-container down
|
||||
db.init:
|
||||
$(MAKE) -C ./dev/db-container init
|
||||
|
||||
.PHONY: psql redis-cli
|
||||
psql:
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
.PHONY: init up down
|
||||
init: down up
|
||||
.PHONY: up down init
|
||||
up:
|
||||
$(COMPOSE) up --detach
|
||||
down:
|
||||
$(COMPOSE) down
|
||||
init:
|
||||
$(COMPOSE) down --volumes
|
||||
$(COMPOSE) up --detach
|
||||
|
||||
.PHONY: psql redis-cli
|
||||
psql:
|
||||
|
|
|
@ -5,6 +5,8 @@ services:
|
|||
image: docker.io/redis:7-alpine
|
||||
ports:
|
||||
- "26379:6379"
|
||||
volumes:
|
||||
- "redis-data:/data"
|
||||
db:
|
||||
image: docker.io/groonga/pgroonga:3.1.8-alpine-12
|
||||
env_file:
|
||||
|
@ -13,3 +15,10 @@ services:
|
|||
- "25432:5432"
|
||||
volumes:
|
||||
- "./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
|
||||
|
|
|
@ -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.
|
||||
- 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)
|
||||
|
||||
- Fix bugs
|
||||
|
|
|
@ -33,6 +33,13 @@ docker 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
|
||||
|
||||
```sh
|
||||
|
|
|
@ -35,7 +35,7 @@ There are official installation instructions for many operating systems on <http
|
|||
|
||||
##### Ubuntu
|
||||
|
||||
1. Add apt repository
|
||||
1. Install subdependencies and add apt repository
|
||||
```sh
|
||||
sudo apt install -y software-properties-common
|
||||
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
|
||||
|
||||
- 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
|
||||
pnpm clean-npm
|
||||
pnpm install
|
||||
|
|
|
@ -21,8 +21,8 @@ searchUser: Търсене на потребител
|
|||
reply: Отговор
|
||||
showMore: Покажи още
|
||||
loadMore: Зареди още
|
||||
followRequestAccepted: Заявка за последване приета
|
||||
importAndExport: Импорт/Експорт на Данни
|
||||
followRequestAccepted: Заявката за последване е приета
|
||||
importAndExport: Импорт/експорт на данни
|
||||
import: Импортиране
|
||||
download: Изтегляне
|
||||
export: Експортиране
|
||||
|
@ -47,7 +47,7 @@ groups: Групи
|
|||
incorrectPassword: Грешна парола.
|
||||
leaveGroup: Напускане на групата
|
||||
numberOfColumn: Брой колони
|
||||
passwordLessLogin: Вход без парола
|
||||
passwordLessLogin: Влизане без парола
|
||||
newPasswordRetype: Повтори новата парола
|
||||
saveAs: Запазване като...
|
||||
resetPassword: Нулиране на паролата
|
||||
|
@ -56,13 +56,13 @@ inputNewFolderName: Въведи ново име на папка
|
|||
upload: Качване
|
||||
retypedNotMatch: Въвежданията не съвпадат.
|
||||
_ago:
|
||||
weeksAgo: преди {n}сед
|
||||
secondsAgo: преди {n}сек
|
||||
hoursAgo: преди {n}ч
|
||||
minutesAgo: преди {n}мин
|
||||
daysAgo: преди {n}д
|
||||
monthsAgo: преди {n}мес
|
||||
yearsAgo: преди {n}г
|
||||
weeksAgo: пр. {n}сед
|
||||
secondsAgo: пр. {n}сек
|
||||
hoursAgo: пр. {n}ч
|
||||
minutesAgo: пр. {n}мин
|
||||
daysAgo: пр. {n}д
|
||||
monthsAgo: пр. {n}мес
|
||||
yearsAgo: пр. {n}г
|
||||
future: Бъдеще
|
||||
justNow: Току-що
|
||||
folderName: Име на папка
|
||||
|
@ -101,6 +101,8 @@ _profile:
|
|||
metadataLabel: Етикет
|
||||
metadataEdit: Редактиране на допълнителната информация
|
||||
changeAvatar: Промяна на профилната снимка
|
||||
youCanIncludeHashtags: Можеш също да включиш хаштагове в биографията си.
|
||||
changeBanner: Промяна на банера
|
||||
addAccount: Добавяне на акаунт
|
||||
followRequestPending: Заявка за последване в изчакване
|
||||
signinHistory: История на вписванията
|
||||
|
@ -108,7 +110,7 @@ or: Или
|
|||
noUsers: Няма потребители
|
||||
notes: Публикации
|
||||
newNoteRecived: Има нови публикации
|
||||
note: Публикуване
|
||||
note: Публикация
|
||||
instanceFollowing: Последвани на сървъра
|
||||
_filters:
|
||||
followersOnly: Само последователи
|
||||
|
@ -170,7 +172,7 @@ renotesCount: Брой изпратени подсилвания
|
|||
license: Лиценз
|
||||
lastUsedDate: Последно използвано на
|
||||
rename: Преименуване
|
||||
customEmojis: Персонализирани емоджита
|
||||
customEmojis: Персон. емоджита
|
||||
emoji: Емоджи
|
||||
_aboutFirefish:
|
||||
translation: Преведи Firefish
|
||||
|
@ -237,6 +239,7 @@ _theme:
|
|||
installedThemes: Инсталирани теми
|
||||
constant: Константа
|
||||
addConstant: Добавяне на константа
|
||||
make: Направа на тема
|
||||
_pages:
|
||||
script:
|
||||
blocks:
|
||||
|
@ -369,8 +372,8 @@ enterUsername: Въведи потребителско име
|
|||
renotedBy: Подсилено от {user}
|
||||
noNotifications: Няма известия
|
||||
instance: Сървър
|
||||
basicSettings: Основни Настройки
|
||||
otherSettings: Други Настройки
|
||||
basicSettings: Основни настройки
|
||||
otherSettings: Други настройки
|
||||
openInWindow: Отваряне в прозорец
|
||||
profile: Профил
|
||||
timeline: Инфопоток
|
||||
|
@ -408,7 +411,7 @@ following: Последвани
|
|||
followsYou: Следва те
|
||||
createList: Създаване на списък
|
||||
error: Грешка
|
||||
manageLists: Управление на списъци
|
||||
manageLists: Управление на списъците
|
||||
retry: Повторен опит
|
||||
follow: Последване
|
||||
followRequest: Заявка за последване
|
||||
|
@ -422,7 +425,7 @@ enterEmoji: Въведи емоджи
|
|||
sensitive: Деликатно
|
||||
add: Добавяне
|
||||
pinned: Закачено в профила
|
||||
quote: Цитиране
|
||||
quote: Цитат
|
||||
pinnedNote: Закачена публикация
|
||||
cantReRenote: Подсилване не може да бъде подсилено.
|
||||
clickToShow: Щракни за показване
|
||||
|
@ -555,7 +558,7 @@ _visibility:
|
|||
followers: Последователи
|
||||
specified: Директна
|
||||
localOnly: Само местни
|
||||
public: Публична
|
||||
public: Общодостъпна
|
||||
publicDescription: Публикацията ще бъде видима във всички публични инфопотоци
|
||||
home: Скрита
|
||||
localOnlyDescription: Не е видима за отдалечени потребители
|
||||
|
@ -638,7 +641,7 @@ _preferencesBackups:
|
|||
updatedAt: 'Обновено на: {date} {time}'
|
||||
editWidgetsExit: Готово
|
||||
done: Готово
|
||||
emailRequiredForSignup: Изискване на адрес на ел. поща за регистриране
|
||||
emailRequiredForSignup: Изискване на адрес за ел. поща за регистриране
|
||||
preview: Преглед
|
||||
privacy: Поверителност
|
||||
about: Относно
|
||||
|
@ -665,14 +668,14 @@ imageUrl: URL адрес на изображение
|
|||
announcements: Оповестявания
|
||||
removeAreYouSure: Сигурни ли сте, че искате да премахнете "{x}"?
|
||||
fromUrl: От URL адрес
|
||||
manageGroups: Управление на групи
|
||||
manageGroups: Управление на групите
|
||||
nUsersRead: прочетено от {n}
|
||||
home: Начало
|
||||
registeredDate: Присъединяване
|
||||
avatar: Профилна снимка
|
||||
watch: Наблюдаване
|
||||
antennas: Антени
|
||||
manageAntennas: Управление на антени
|
||||
manageAntennas: Управление на антените
|
||||
popularTags: Популярни тагове
|
||||
cacheClear: Изчистване на кеша
|
||||
groupName: Име на групата
|
||||
|
@ -700,7 +703,7 @@ gallery: Галерия
|
|||
priority: Приоритет
|
||||
unread: Непрочетени
|
||||
filter: Филтриране
|
||||
manageAccounts: Управление на акаунти
|
||||
manageAccounts: Управление на акаунтите
|
||||
deleteAccount: Изтриване на акаунта
|
||||
fast: Бърза
|
||||
remoteOnly: Само отдалечени
|
||||
|
@ -785,6 +788,9 @@ _menuDisplay:
|
|||
hide: Скриване
|
||||
_exportOrImport:
|
||||
allNotes: Всички публикации
|
||||
followingList: Следвани потребители
|
||||
blockingList: Блокирани потребители
|
||||
muteList: Заглушени потребители
|
||||
exploreFediverse: Разглеждане на Федивселената
|
||||
recentlyUpdatedUsers: Последно активни потребители
|
||||
uiLanguage: Език на потребителския интерфейс
|
||||
|
@ -793,7 +799,7 @@ tags: Тагове
|
|||
youHaveNoGroups: Нямаш групи
|
||||
accessibility: Достъпност
|
||||
email: Ел. поща
|
||||
emailAddress: Адрес на ел. поща
|
||||
emailAddress: Адрес за ел. поща
|
||||
addItem: Добавяне на елемент
|
||||
visibility: Видимост
|
||||
description: Описание
|
||||
|
@ -867,3 +873,63 @@ unblockConfirm: Сигурни ли сте, че искате да отблок
|
|||
followConfirm: Сигурни ли сте, че искате да последвате {name}?
|
||||
accountMoved: 'Потребителят се премести на нов акаунт:'
|
||||
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: Автоматично откриване на езика и показване на бутон за превеждане
|
||||
за публикации на чужди езици
|
||||
|
|
|
@ -856,8 +856,8 @@ more: Més!
|
|||
featured: Destacat
|
||||
usernameOrUserId: Nom o ID d'usuari
|
||||
noSuchUser: No s'ha trobat l'usuari
|
||||
lookup: Cerca
|
||||
attachFile: Afegeix un fitxer
|
||||
lookup: Ves a
|
||||
attachFile: Afegeix fitxers
|
||||
currentPassword: Contrasenya actual
|
||||
newPassword: Nova contrasenya
|
||||
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
|
||||
compte
|
||||
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
|
||||
showBigPostButton: Mostrar un botó gegant al formulari de publicació
|
||||
_emojiModPerm:
|
||||
|
@ -2238,7 +2238,7 @@ enableTimelineStreaming: Actualitza les línies de temps automàticament
|
|||
enablePullToRefresh: Activa "Baixa per actualitzar"
|
||||
pullDownToReload: Baixa 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
|
||||
sentFollowRequests: Enviar sol·licituds de seguiment
|
||||
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
|
||||
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
|
||||
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."
|
||||
searchPostsWithFiles: Només publicacions amb fitxers
|
||||
searchCwAndAlt: Inclou avisos de contingut i arxius amb descripcions
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "firefish",
|
||||
"version": "20240326",
|
||||
"version": "20240401",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
|
@ -20,7 +20,7 @@
|
|||
"watch": "pnpm run dev",
|
||||
"dev": "pnpm node ./scripts/dev.mjs",
|
||||
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
||||
"lint": "pnpm -r --parallel run lint",
|
||||
"lint": "pnpm --filter !megalodon --filter !firefish-js -r --parallel run lint",
|
||||
"debug": "pnpm run build:debug && pnpm run start",
|
||||
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
|
||||
"mocha": "pnpm --filter backend run mocha",
|
||||
|
|
|
@ -4,7 +4,6 @@ Cargo.lock
|
|||
.github
|
||||
npm
|
||||
.eslintrc
|
||||
.prettierignore
|
||||
rustfmt.toml
|
||||
yarn.lock
|
||||
*.node
|
||||
|
|
|
@ -8,10 +8,15 @@ import chalk from "chalk";
|
|||
import Logger from "@/services/logger.js";
|
||||
import IPCIDR from "ip-cidr";
|
||||
import PrivateIp from "private-ip";
|
||||
import { isValidUrl } from "./is-valid-url.js";
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
if (!isValidUrl(url)) {
|
||||
throw new StatusError("Invalid URL", 400);
|
||||
}
|
||||
|
||||
const logger = new Logger("download");
|
||||
|
||||
logger.info(`Downloading ${chalk.cyan(url)} ...`);
|
||||
|
@ -44,6 +49,12 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||
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) => {
|
||||
if (
|
||||
(process.env.NODE_ENV === "production" ||
|
||||
|
|
|
@ -2,9 +2,10 @@ import * as http from "node:http";
|
|||
import * as https from "node:https";
|
||||
import type { URL } from "node:url";
|
||||
import CacheableLookup from "cacheable-lookup";
|
||||
import fetch from "node-fetch";
|
||||
import fetch, { RequestRedirect } from "node-fetch";
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
|
||||
import config from "@/config/index.js";
|
||||
import { isValidUrl } from "./is-valid-url.js";
|
||||
|
||||
export async function getJson(
|
||||
url: string,
|
||||
|
@ -57,7 +58,12 @@ export async function getResponse(args: {
|
|||
headers: Record<string, string>;
|
||||
timeout?: number;
|
||||
size?: number;
|
||||
redirect?: RequestRedirect;
|
||||
}) {
|
||||
if (!isValidUrl(args.url)) {
|
||||
throw new StatusError("Invalid URL", 400);
|
||||
}
|
||||
|
||||
const timeout = args.timeout || 10 * 1000;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
@ -73,8 +79,16 @@ export async function getResponse(args: {
|
|||
size: args.size || 10 * 1024 * 1024,
|
||||
agent: getAgentByUrl,
|
||||
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) {
|
||||
throw new StatusError(
|
||||
`${res.status} ${res.statusText}`,
|
||||
|
|
20
packages/backend/src/misc/is-valid-url.ts
Normal file
20
packages/backend/src/misc/is-valid-url.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
import config from "@/config/index.js";
|
||||
import { getUserKeypair } from "@/misc/keypair-store.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 type { Response } from "node-fetch";
|
||||
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) => {
|
||||
const body = JSON.stringify(object);
|
||||
|
@ -33,10 +35,19 @@ export default async (user: { id: User["id"] }, url: string, object: any) => {
|
|||
|
||||
/**
|
||||
* Get ActivityPub object
|
||||
* @param user http-signature user
|
||||
* @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;
|
||||
|
||||
if (user != null) {
|
||||
|
@ -56,7 +67,16 @@ export async function apGet(url: string, user?: ILocalUser): Promise<IObject> {
|
|||
url,
|
||||
method: req.request.method,
|
||||
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 {
|
||||
res = await getResponse({
|
||||
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"',
|
||||
"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");
|
||||
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");
|
||||
|
@ -79,7 +110,10 @@ export async function apGet(url: string, user?: ILocalUser): Promise<IObject> {
|
|||
const text = await res.text();
|
||||
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 {
|
||||
|
|
|
@ -6,7 +6,13 @@ import { extractDbHost, isSelfHost } from "@/misc/convert-host.js";
|
|||
import { apGet } from "./request.js";
|
||||
import type { IObject, ICollection, IOrderedCollection } 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 renderNote from "@/remote/activitypub/renderer/note.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 { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
import { apLogger } from "@/remote/activitypub/logger.js";
|
||||
import { In, IsNull, Not } from "typeorm";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
|
||||
export default class Resolver {
|
||||
private history: Set<string>;
|
||||
|
@ -114,7 +120,7 @@ export default class Resolver {
|
|||
apLogger.debug("Getting object from remote, authenticated as user:");
|
||||
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 (
|
||||
object == null ||
|
||||
|
@ -127,61 +133,73 @@ export default class Resolver {
|
|||
throw new Error("invalid response");
|
||||
}
|
||||
|
||||
return object;
|
||||
if (object.id == null) {
|
||||
throw new Error("Object has no ID");
|
||||
}
|
||||
|
||||
private resolveLocal(url: string): Promise<IObject> {
|
||||
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 async resolveLocal(url: string): Promise<IObject> {
|
||||
const parsed = parseUri(url);
|
||||
if (!parsed.local) throw new Error("resolveLocal: not local");
|
||||
|
||||
switch (parsed.type) {
|
||||
case "notes":
|
||||
return Notes.findOneByOrFail({ id: parsed.id }).then((note) => {
|
||||
const note = await Notes.findOneByOrFail({ id: parsed.id });
|
||||
if (parsed.rest === "activity") {
|
||||
// this refers to the create activity and not the note itself
|
||||
return renderActivity(renderCreate(renderNote(note), note));
|
||||
} else {
|
||||
return renderNote(note);
|
||||
}
|
||||
});
|
||||
case "users":
|
||||
return Users.findOneByOrFail({ id: parsed.id }).then((user) =>
|
||||
renderPerson(user as ILocalUser),
|
||||
);
|
||||
const user = await Users.findOneByOrFail({ id: parsed.id });
|
||||
return await renderPerson(user as ILocalUser);
|
||||
case "questions":
|
||||
// Polls are indexed by the note they are attached to.
|
||||
return Promise.all([
|
||||
const [pollNote, poll] = await Promise.all([
|
||||
Notes.findOneByOrFail({ id: 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":
|
||||
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(
|
||||
(reaction) => renderActivity(renderLike(reaction, { uri: null })),
|
||||
);
|
||||
const reaction = await NoteReactions.findOneByOrFail({ id: parsed.id });
|
||||
return renderActivity(renderLike(reaction, { uri: null }));
|
||||
case "follows":
|
||||
// if rest is a <followee id>
|
||||
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 })),
|
||||
).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.
|
||||
const followRequest = FollowRequests.findOneBy({
|
||||
const followRequest = await FollowRequests.findOneBy({
|
||||
id: parsed.id,
|
||||
});
|
||||
if (followRequest == null) {
|
||||
throw new Error("resolveLocal: invalid follow URI");
|
||||
}
|
||||
const follower = Users.findOneBy({
|
||||
const follower = await Users.findOneBy({
|
||||
id: followRequest.followerId,
|
||||
host: IsNull(),
|
||||
});
|
||||
const followee = Users.findOneBy({
|
||||
const followee = await Users.findOneBy({
|
||||
id: followRequest.followeeId,
|
||||
host: Not(IsNull()),
|
||||
});
|
||||
|
@ -190,7 +208,7 @@ export default class Resolver {
|
|||
}
|
||||
return renderActivity(renderFollow(follower, followee, url));
|
||||
default:
|
||||
throw new Error(`resolveLocal: type ${type} unhandled`);
|
||||
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ export default async function (ctx: Koa.Context) {
|
|||
return;
|
||||
}
|
||||
|
||||
ctx.set("X-Content-Type-Options", "nosniff");
|
||||
|
||||
const isThumbnail = file.thumbnailAccessKey === key;
|
||||
const isWebpublic = file.webpublicAccessKey === key;
|
||||
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
{
|
||||
"extends": ["@eslint-sets/vue3", "@eslint-sets/vue3-ts"],
|
||||
"plugins": ["file-progress", "prettier"],
|
||||
"plugins": ["file-progress"],
|
||||
"ignorePatterns": ["**/*.json5"],
|
||||
"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": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"build:debug": "pnpm run build",
|
||||
"lint": "pnpm biome check **/*.ts --apply ; pnpm run lint:vue",
|
||||
"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": {
|
||||
"@eslint-sets/eslint-config-vue3": "^5.12.0",
|
||||
|
@ -48,7 +48,6 @@
|
|||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "3.6.0",
|
||||
"emojilib": "^3.0.11",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-file-progress": "^1.3.0",
|
||||
"eventemitter3": "5.0.1",
|
||||
"fast-blurhash": "^1.1.2",
|
||||
|
@ -65,7 +64,6 @@
|
|||
"mfm-js": "0.24.0",
|
||||
"moment": "2.30.1",
|
||||
"photoswipe": "5.4.3",
|
||||
"prettier": "3.2.5",
|
||||
"prismjs": "1.29.0",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.13.0",
|
||||
|
|
|
@ -179,8 +179,7 @@ async function renderActiveUsersChart() {
|
|||
enabled: false,
|
||||
callbacks: {
|
||||
title(context) {
|
||||
const v =
|
||||
context[0].dataset.data[context[0].dataIndex];
|
||||
const v = context[0].dataset.data[context[0].dataIndex];
|
||||
return v.d;
|
||||
},
|
||||
label(context) {
|
||||
|
|
|
@ -166,21 +166,21 @@ const texts = computed(() => {
|
|||
return angles;
|
||||
});
|
||||
|
||||
let enabled = true,
|
||||
majorGraduationColor = ref<string>(),
|
||||
let enabled = true;
|
||||
const majorGraduationColor = ref<string>();
|
||||
// let minorGraduationColor = $ref<string>();
|
||||
sHandColor = ref<string>(),
|
||||
mHandColor = ref<string>(),
|
||||
hHandColor = ref<string>(),
|
||||
nowColor = ref<string>(),
|
||||
h = ref<number>(0),
|
||||
m = ref<number>(0),
|
||||
s = ref<number>(0),
|
||||
hAngle = ref<number>(0),
|
||||
mAngle = ref<number>(0),
|
||||
sAngle = ref<number>(0),
|
||||
disableSAnimate = ref(false),
|
||||
sOneRound = false;
|
||||
const sHandColor = ref<string>();
|
||||
const mHandColor = ref<string>();
|
||||
const hHandColor = ref<string>();
|
||||
const nowColor = ref<string>();
|
||||
const h = ref<number>(0);
|
||||
const m = ref<number>(0);
|
||||
const s = ref<number>(0);
|
||||
const hAngle = ref<number>(0);
|
||||
const mAngle = ref<number>(0);
|
||||
const sAngle = ref<number>(0);
|
||||
const disableSAnimate = ref(false);
|
||||
let sOneRound = false;
|
||||
|
||||
function tick() {
|
||||
const now = new Date();
|
||||
|
|
|
@ -128,8 +128,8 @@ const getColor = (i) => {
|
|||
};
|
||||
|
||||
const now = new Date();
|
||||
let chartInstance: Chart = null,
|
||||
chartData: {
|
||||
let chartInstance: Chart = null;
|
||||
let chartData: {
|
||||
series: {
|
||||
name: string;
|
||||
type: "line" | "area";
|
||||
|
|
|
@ -33,8 +33,8 @@ const rootEl = ref<HTMLDivElement>();
|
|||
const zIndex = ref<number>(os.claimZIndex("high"));
|
||||
|
||||
onMounted(() => {
|
||||
let left = props.ev.pageX + 1, // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||
|
||||
const width = rootEl.value.offsetWidth;
|
||||
const height = rootEl.value.offsetHeight;
|
||||
|
|
|
@ -64,8 +64,8 @@ const imgUrl = `${url}/proxy/image.webp?${query({
|
|||
})}`;
|
||||
const dialogEl = ref<InstanceType<typeof XModalWindow>>();
|
||||
const imgEl = ref<HTMLImageElement>();
|
||||
let cropper: Cropper | null = null,
|
||||
loading = ref(true);
|
||||
let cropper: Cropper | null = null;
|
||||
const loading = ref(true);
|
||||
|
||||
const ok = async () => {
|
||||
const promise = new Promise<entities.DriveFile>(async (res) => {
|
||||
|
|
|
@ -83,9 +83,7 @@ function getMenu() {
|
|||
text: props.file.isSensitive
|
||||
? i18n.ts.unmarkAsSensitive
|
||||
: i18n.ts.markAsSensitive,
|
||||
icon: props.file.isSensitive
|
||||
? "ph-eye ph-lg"
|
||||
: "ph-eye-slash ph-lg",
|
||||
icon: props.file.isSensitive ? "ph-eye ph-lg" : "ph-eye-slash ph-lg",
|
||||
action: toggleSensitive,
|
||||
},
|
||||
{
|
||||
|
@ -129,9 +127,7 @@ function onClick(ev: MouseEvent) {
|
|||
} else {
|
||||
os.popupMenu(
|
||||
getMenu(),
|
||||
(ev.currentTarget ?? ev.target ?? undefined) as
|
||||
| HTMLElement
|
||||
| undefined,
|
||||
(ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -451,9 +451,7 @@ function chooseFile(file: entities.DriveFile) {
|
|||
const isAlreadySelected = selectedFiles.value.some((f) => f.id === file.id);
|
||||
if (props.multiple) {
|
||||
if (isAlreadySelected) {
|
||||
selectedFiles.value = selectedFiles.value.filter(
|
||||
(f) => f.id !== file.id,
|
||||
);
|
||||
selectedFiles.value = selectedFiles.value.filter((f) => f.id !== file.id);
|
||||
} else {
|
||||
selectedFiles.value.push(file);
|
||||
}
|
||||
|
|
|
@ -113,19 +113,19 @@
|
|||
<header>{{ i18n.ts.emoji }}</header>
|
||||
<XSection
|
||||
v-for="category in unicodeEmojiCategories"
|
||||
:key="category"
|
||||
:skin-tone-selector="category === 'people'"
|
||||
:key="category.slug"
|
||||
:skin-tone-selector="category.slug === 'people_body'"
|
||||
:skin-tones="unicodeEmojiSkinTones"
|
||||
:skin-tone-labels="unicodeEmojiSkinToneLabels"
|
||||
:emojis="
|
||||
emojilist
|
||||
.filter((e) => e.category === category)
|
||||
.filter(
|
||||
(e) => e.category_slug === category.slug,
|
||||
)
|
||||
.map((e) => e.emoji)
|
||||
"
|
||||
@chosen="chosen"
|
||||
>{{
|
||||
getNicelyLabeledCategory(category) || category
|
||||
}}</XSection
|
||||
>{{ category.name }}</XSection
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -169,11 +169,7 @@ import type { entities } from "firefish-js";
|
|||
import { FocusTrap } from "focus-trap-vue";
|
||||
import XSection from "@/components/MkEmojiPicker.section.vue";
|
||||
import type { UnicodeEmojiDef } from "@/scripts/emojilist";
|
||||
import {
|
||||
emojilist,
|
||||
getNicelyLabeledCategory,
|
||||
unicodeEmojiCategories,
|
||||
} from "@/scripts/emojilist";
|
||||
import { emojilist, unicodeEmojiCategories } from "@/scripts/emojilist";
|
||||
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
|
||||
import Ripple from "@/components/MkRipple.vue";
|
||||
import * as os from "@/os";
|
||||
|
@ -284,9 +280,7 @@ watch(q, () => {
|
|||
keywords.every(
|
||||
(keyword) =>
|
||||
emoji.name.includes(keyword) ||
|
||||
emoji.aliases.some((alias) =>
|
||||
alias.includes(keyword),
|
||||
),
|
||||
emoji.aliases.some((alias) => alias.includes(keyword)),
|
||||
)
|
||||
) {
|
||||
matches.add(emoji);
|
||||
|
@ -356,9 +350,7 @@ watch(q, () => {
|
|||
keywords.every(
|
||||
(keyword) =>
|
||||
emoji.slug.includes(keyword) ||
|
||||
emoji.keywords?.some((alias) =>
|
||||
alias.includes(keyword),
|
||||
),
|
||||
emoji.keywords?.some((alias) => alias.includes(keyword)),
|
||||
)
|
||||
) {
|
||||
matches.add(emoji);
|
||||
|
@ -375,9 +367,7 @@ watch(q, () => {
|
|||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (
|
||||
emoji.keywords?.some((keyword) => keyword.startsWith(newQ))
|
||||
) {
|
||||
if (emoji.keywords?.some((keyword) => keyword.startsWith(newQ))) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
}
|
||||
|
@ -428,8 +418,7 @@ function getKey(
|
|||
|
||||
function chosen(emoji: any, ev?: MouseEvent) {
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + el.offsetWidth / 2;
|
||||
|
|
|
@ -58,9 +58,7 @@ export default defineComponent({
|
|||
showBody:
|
||||
this.persistKey &&
|
||||
localStorage.getItem(localStoragePrefix + this.persistKey)
|
||||
? localStorage.getItem(
|
||||
localStoragePrefix + this.persistKey,
|
||||
) === "t"
|
||||
? localStorage.getItem(localStoragePrefix + this.persistKey) === "t"
|
||||
: this.expanded,
|
||||
animation: defaultStore.state.animation,
|
||||
};
|
||||
|
|
|
@ -112,8 +112,7 @@ if (props.user.isFollowing == null) {
|
|||
function onFollowChange(user: entities.UserDetailed) {
|
||||
if (user.id === props.user.id) {
|
||||
isFollowing.value = user.isFollowing;
|
||||
hasPendingFollowRequestFromYou.value =
|
||||
user.hasPendingFollowRequestFromYou;
|
||||
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,10 +171,7 @@ async function onClick() {
|
|||
}
|
||||
|
||||
function menu(ev) {
|
||||
os.popupMenu(
|
||||
getUserMenu(props.user, router),
|
||||
ev.currentTarget ?? ev.target,
|
||||
);
|
||||
os.popupMenu(getUserMenu(props.user, router), ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
@ -22,14 +22,14 @@
|
|||
@load="onLoad"
|
||||
/>
|
||||
<i
|
||||
class="alt-indicator"
|
||||
:class="icon('ph-subtitles')"
|
||||
v-if="alt && showAltIndicator"
|
||||
v-tooltip.noLabel="
|
||||
`${i18n.ts.alt}: ${
|
||||
alt.length > 200 ? alt.trim().slice(0, 200) + '...' : alt.trim()
|
||||
}`
|
||||
"
|
||||
class="alt-indicator"
|
||||
:class="icon('ph-subtitles')"
|
||||
></i>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -38,9 +38,7 @@ const instance = props.instance ?? {
|
|||
faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico",
|
||||
name: instanceName,
|
||||
themeColor: (
|
||||
document.querySelector(
|
||||
'meta[name="theme-color-orig"]',
|
||||
) as HTMLMetaElement
|
||||
document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement
|
||||
)?.content,
|
||||
softwareName: Instance.softwareName ?? "Firefish",
|
||||
softwareVersion: version,
|
||||
|
|
|
@ -40,9 +40,7 @@ const el = ref();
|
|||
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkUrlPreviewPopup.vue"),
|
||||
),
|
||||
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
|
||||
{
|
||||
showing,
|
||||
url: props.url,
|
||||
|
|
|
@ -58,9 +58,7 @@ export default {
|
|||
{
|
||||
class: $style.text,
|
||||
style: {
|
||||
animationDirection: reverse
|
||||
? "reverse"
|
||||
: undefined,
|
||||
animationDirection: reverse ? "reverse" : undefined,
|
||||
},
|
||||
},
|
||||
$slots.default(),
|
||||
|
|
|
@ -150,8 +150,7 @@ watch(
|
|||
hide.value =
|
||||
defaultStore.state.nsfw === "force"
|
||||
? true
|
||||
: props.media.isSensitive &&
|
||||
defaultStore.state.nsfw !== "ignore";
|
||||
: props.media.isSensitive && defaultStore.state.nsfw !== "ignore";
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
|
|
|
@ -75,8 +75,7 @@ const audioEl = ref<HTMLAudioElement | null>();
|
|||
const hide = ref(true);
|
||||
|
||||
function volumechange() {
|
||||
if (audioEl.value)
|
||||
ColdDeviceStorage.set("mediaVolume", audioEl.value.volume);
|
||||
if (audioEl.value) ColdDeviceStorage.set("mediaVolume", audioEl.value.volume);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
@ -183,12 +183,8 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
caption() {
|
||||
const img = document.getElementById(
|
||||
"imgtocaption",
|
||||
) as HTMLImageElement;
|
||||
const ta = document.getElementById(
|
||||
"captioninput",
|
||||
) as HTMLTextAreaElement;
|
||||
const img = document.getElementById("imgtocaption") as HTMLImageElement;
|
||||
const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
|
||||
os.api("drive/files/caption-image", {
|
||||
url: img.src,
|
||||
}).then((text) => {
|
||||
|
|
|
@ -168,10 +168,10 @@ function load() {
|
|||
|
||||
onMounted(load);
|
||||
|
||||
let currentRow = 0,
|
||||
rowHeight = 0,
|
||||
buffer = null,
|
||||
isSeeking = false;
|
||||
let currentRow = 0;
|
||||
let rowHeight = 0;
|
||||
let buffer = null;
|
||||
const isSeeking = false;
|
||||
|
||||
function captionPopup() {
|
||||
os.alert({
|
||||
|
@ -311,11 +311,7 @@ function getRow(pattern: number, rowOffset: number) {
|
|||
const ops: string[] = [];
|
||||
|
||||
for (let channel = 0; channel < nbChannels.value; channel++) {
|
||||
const part = player.value.getPatternRowChannel(
|
||||
pattern,
|
||||
rowOffset,
|
||||
channel,
|
||||
);
|
||||
const part = player.value.getPatternRowChannel(pattern, rowOffset, channel);
|
||||
|
||||
notes.push(part.substring(0, 3));
|
||||
insts.push(part.substring(4, 6));
|
||||
|
|
|
@ -235,7 +235,8 @@ const align = () => {
|
|||
const width = content.value!.offsetWidth;
|
||||
const height = content.value!.offsetHeight;
|
||||
|
||||
let left, top;
|
||||
let left;
|
||||
let top;
|
||||
|
||||
const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
|
||||
const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
|
||||
|
@ -286,8 +287,7 @@ const align = () => {
|
|||
left = window.innerWidth - width + window.scrollX - 1;
|
||||
}
|
||||
|
||||
const underSpace =
|
||||
window.innerHeight - MARGIN - (top - window.pageYOffset);
|
||||
const underSpace = window.innerHeight - MARGIN - (top - window.pageYOffset);
|
||||
const upperSpace = srcRect.top - MARGIN;
|
||||
|
||||
// 画面から縦にはみ出る場合
|
||||
|
@ -300,12 +300,7 @@ const align = () => {
|
|||
top = window.scrollY + (upperSpace + MARGIN - height);
|
||||
}
|
||||
} else {
|
||||
top =
|
||||
window.innerHeight -
|
||||
MARGIN -
|
||||
height +
|
||||
window.pageYOffset -
|
||||
1;
|
||||
top = window.innerHeight - MARGIN - height + window.pageYOffset - 1;
|
||||
}
|
||||
} else {
|
||||
maxHeight.value = underSpace;
|
||||
|
@ -320,8 +315,8 @@ const align = () => {
|
|||
left = 0;
|
||||
}
|
||||
|
||||
let transformOriginX = "center",
|
||||
transformOriginY = "center";
|
||||
let transformOriginX = "center";
|
||||
let transformOriginY = "center";
|
||||
|
||||
if (
|
||||
top >=
|
||||
|
|
|
@ -412,8 +412,7 @@ async function translate() {
|
|||
lang != null &&
|
||||
translateLang !== lang &&
|
||||
(!translation.value ||
|
||||
translation.value.sourceLang.toLowerCase() ===
|
||||
translateLang.slice(0, 2))
|
||||
translation.value.sourceLang.toLowerCase() === translateLang.slice(0, 2))
|
||||
)
|
||||
translation.value = await translate_(appearNote.value.id, lang);
|
||||
translating.value = false;
|
||||
|
@ -515,10 +514,7 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
icon: `${icon("ph-arrows-out-simple")}`,
|
||||
text: i18n.ts.showInPage,
|
||||
action: () => {
|
||||
router.push(
|
||||
notePage(appearNote.value),
|
||||
"forcePage",
|
||||
);
|
||||
router.push(notePage(appearNote.value), "forcePage");
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
|
@ -542,10 +538,7 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
type: "a",
|
||||
icon: `${icon("ph-arrow-square-up-right")}`,
|
||||
text: i18n.ts.showOnRemote,
|
||||
href:
|
||||
appearNote.value.url ??
|
||||
appearNote.value.uri ??
|
||||
"",
|
||||
href: appearNote.value.url ?? appearNote.value.uri ?? "",
|
||||
target: "_blank",
|
||||
}
|
||||
: undefined,
|
||||
|
|
|
@ -70,8 +70,7 @@ const note = ref(props.note);
|
|||
|
||||
const showTicker =
|
||||
defaultStore.state.instanceTicker === "always" ||
|
||||
(defaultStore.state.instanceTicker === "remote" &&
|
||||
note.value.user.instance);
|
||||
(defaultStore.state.instanceTicker === "remote" && note.value.user.instance);
|
||||
|
||||
function openServerInfo() {
|
||||
if (!props.canOpenServerInfo || !defaultStore.state.openServerInfo) return;
|
||||
|
|
|
@ -280,8 +280,7 @@ const replies: entities.Note[] =
|
|||
props.conversation
|
||||
?.filter(
|
||||
(item) =>
|
||||
item.replyId === props.note.id ||
|
||||
item.renoteId === props.note.id,
|
||||
item.replyId === props.note.id || item.renoteId === props.note.id,
|
||||
)
|
||||
.reverse() ?? [];
|
||||
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
||||
|
@ -319,8 +318,7 @@ async function translate() {
|
|||
lang != null &&
|
||||
translateLang !== lang &&
|
||||
(!translation.value ||
|
||||
translation.value.sourceLang.toLowerCase() ===
|
||||
translateLang.slice(0, 2))
|
||||
translation.value.sourceLang.toLowerCase() === translateLang.slice(0, 2))
|
||||
)
|
||||
translation.value = await translate_(appearNote.value.id, lang);
|
||||
translating.value = false;
|
||||
|
@ -421,10 +419,7 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
icon: `${icon("ph-arrows-out-simple")}`,
|
||||
text: i18n.ts.showInPage,
|
||||
action: () => {
|
||||
router.push(
|
||||
notePage(appearNote.value),
|
||||
"forcePage",
|
||||
);
|
||||
router.push(notePage(appearNote.value), "forcePage");
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
|
|
|
@ -310,7 +310,8 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
|
|||
? instance.defaultReaction
|
||||
: "⭐";
|
||||
|
||||
let readObserver: IntersectionObserver | undefined, connection;
|
||||
let readObserver: IntersectionObserver | undefined;
|
||||
let connection;
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.notification.isRead) {
|
||||
|
|
|
@ -84,9 +84,7 @@ function ok() {
|
|||
} else {
|
||||
emit("done", {
|
||||
includingTypes: (
|
||||
Object.keys(
|
||||
typesMap.value,
|
||||
) as (typeof notificationTypes)[number][]
|
||||
Object.keys(typesMap.value) as (typeof notificationTypes)[number][]
|
||||
).filter((type) => typesMap.value[type]),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -71,9 +71,7 @@ const pagination: Paging = {
|
|||
limit: 10,
|
||||
params: computed(() => ({
|
||||
includeTypes: props.includeTypes ?? undefined,
|
||||
excludeTypes: props.includeTypes
|
||||
? undefined
|
||||
: me.mutingNotificationTypes,
|
||||
excludeTypes: props.includeTypes ? undefined : me.mutingNotificationTypes,
|
||||
unreadOnly: props.unreadOnly,
|
||||
})),
|
||||
};
|
||||
|
@ -114,20 +112,12 @@ onMounted(() => {
|
|||
connection.on("readNotifications", (notificationIds) => {
|
||||
if (pagingComponent.value) {
|
||||
for (let i = 0; i < pagingComponent.value.queue.length; i++) {
|
||||
if (
|
||||
notificationIds.includes(pagingComponent.value.queue[i].id)
|
||||
) {
|
||||
if (notificationIds.includes(pagingComponent.value.queue[i].id)) {
|
||||
pagingComponent.value.queue[i].isRead = true;
|
||||
}
|
||||
}
|
||||
for (
|
||||
let i = 0;
|
||||
i < (pagingComponent.value.items || []).length;
|
||||
i++
|
||||
) {
|
||||
if (
|
||||
notificationIds.includes(pagingComponent.value.items[i].id)
|
||||
) {
|
||||
for (let i = 0; i < (pagingComponent.value.items || []).length; i++) {
|
||||
if (notificationIds.includes(pagingComponent.value.items[i].id)) {
|
||||
pagingComponent.value.items[i].isRead = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,14 +164,10 @@ const init = async (): Promise<void> => {
|
|||
res.length > (props.pagination.limit || 10)
|
||||
) {
|
||||
res.pop();
|
||||
items.value = props.pagination.reversed
|
||||
? [...res].reverse()
|
||||
: res;
|
||||
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
||||
more.value = true;
|
||||
} else {
|
||||
items.value = props.pagination.reversed
|
||||
? [...res].reverse()
|
||||
: res;
|
||||
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
||||
more.value = false;
|
||||
}
|
||||
offset.value = res.length;
|
||||
|
@ -381,8 +377,7 @@ const prepend = (item: Item): void => {
|
|||
|
||||
const isTop =
|
||||
isBackTop.value ||
|
||||
(document.body.contains(rootEl.value) &&
|
||||
isTopVisible(rootEl.value));
|
||||
(document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
||||
|
||||
if (isTop) {
|
||||
// Prepend the item
|
||||
|
|
|
@ -74,8 +74,7 @@ const closed = computed(() => remaining.value === 0);
|
|||
const isLocal = computed(() => !props.note.uri);
|
||||
const isVoted = computed(
|
||||
() =>
|
||||
!props.note.poll.multiple &&
|
||||
props.note.poll.choices.some((c) => c.isVoted),
|
||||
!props.note.poll.multiple && props.note.poll.choices.some((c) => c.isVoted),
|
||||
);
|
||||
const timer = computed(() =>
|
||||
i18n.t(
|
||||
|
@ -101,10 +100,8 @@ const showResult = ref(props.readOnly || isVoted.value);
|
|||
if (props.note.poll.expiresAt) {
|
||||
const tick = () => {
|
||||
remaining.value = Math.floor(
|
||||
Math.max(
|
||||
new Date(props.note.poll.expiresAt).getTime() - Date.now(),
|
||||
0,
|
||||
) / 1000,
|
||||
Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) /
|
||||
1000,
|
||||
);
|
||||
if (remaining.value === 0) {
|
||||
showResult.value = true;
|
||||
|
|
|
@ -331,8 +331,8 @@ import { vibrate } from "@/scripts/vibrate";
|
|||
import { langmap } from "@/scripts/langmap";
|
||||
import {
|
||||
detectLanguage,
|
||||
isSupportedLang,
|
||||
isSameLanguage,
|
||||
isSupportedLang,
|
||||
languageContains,
|
||||
parentLanguage,
|
||||
} from "@/scripts/language-utils";
|
||||
|
@ -527,9 +527,7 @@ if (
|
|||
(props.reply.user.host != null && props.reply.user.host !== host))
|
||||
) {
|
||||
text.value = `@${props.reply.user.username}${
|
||||
props.reply.user.host != null
|
||||
? "@" + toASCII(props.reply.user.host)
|
||||
: ""
|
||||
props.reply.user.host != null ? "@" + toASCII(props.reply.user.host) : ""
|
||||
} `;
|
||||
}
|
||||
|
||||
|
@ -589,11 +587,9 @@ if (
|
|||
}
|
||||
|
||||
if (props.reply.userId !== me.id) {
|
||||
os.api("users/show", { userId: props.reply.userId }).then(
|
||||
(user) => {
|
||||
os.api("users/show", { userId: props.reply.userId }).then((user) => {
|
||||
pushVisibleUser(user);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -732,9 +728,7 @@ function setVisibility() {
|
|||
}
|
||||
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkVisibilityPicker.vue"),
|
||||
),
|
||||
defineAsyncComponent(() => import("@/components/MkVisibilityPicker.vue")),
|
||||
{
|
||||
currentVisibility: visibility.value,
|
||||
currentLocalOnly: localOnly.value,
|
||||
|
@ -834,8 +828,7 @@ function setLanguage() {
|
|||
|
||||
for (const lang of langs) {
|
||||
if (lang === language.value) continue;
|
||||
if (defaultStore.state.recentlyUsedPostLanguages.includes(lang))
|
||||
continue;
|
||||
if (defaultStore.state.recentlyUsedPostLanguages.includes(lang)) continue;
|
||||
actions.push({
|
||||
text: langmap[lang].nativeName,
|
||||
danger: false,
|
||||
|
@ -1072,8 +1065,7 @@ async function post() {
|
|||
let postData = {
|
||||
editId: props.editId ? props.editId : undefined,
|
||||
text: processedText === "" ? undefined : processedText,
|
||||
fileIds:
|
||||
files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
|
||||
fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
|
||||
replyId: props.reply ? props.reply.id : undefined,
|
||||
renoteId: props.renote
|
||||
? props.renote.id
|
||||
|
@ -1085,8 +1077,7 @@ async function post() {
|
|||
cw: useCw.value ? cw.value || "" : undefined,
|
||||
lang: language.value ? language.value : undefined,
|
||||
localOnly: localOnly.value,
|
||||
visibility:
|
||||
visibility.value === "private" ? "specified" : visibility.value,
|
||||
visibility: visibility.value === "private" ? "specified" : visibility.value,
|
||||
visibleUserIds:
|
||||
visibility.value === "private"
|
||||
? []
|
||||
|
@ -1101,9 +1092,7 @@ async function post() {
|
|||
.split(" ")
|
||||
.map((x) => (x.startsWith("#") ? x : "#" + x))
|
||||
.join(" ");
|
||||
postData.text = postData.text
|
||||
? `${postData.text} ${hashtags_}`
|
||||
: hashtags_;
|
||||
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
|
||||
}
|
||||
|
||||
// plugin
|
||||
|
@ -1117,9 +1106,7 @@ async function post() {
|
|||
|
||||
if (postAccount.value) {
|
||||
const storedAccounts = await getAccounts();
|
||||
token = storedAccounts.find(
|
||||
(x) => x.id === postAccount.value.id,
|
||||
)?.token;
|
||||
token = storedAccounts.find((x) => x.id === postAccount.value.id)?.token;
|
||||
}
|
||||
|
||||
posting.value = true;
|
||||
|
@ -1165,14 +1152,9 @@ async function post() {
|
|||
"recentlyUsedPostLanguages",
|
||||
[language.value]
|
||||
.concat(
|
||||
defaultStore.state.recentlyUsedPostLanguages.filter(
|
||||
(lang) => {
|
||||
return (
|
||||
lang !== language.value &&
|
||||
languages.includes(lang)
|
||||
);
|
||||
},
|
||||
),
|
||||
defaultStore.state.recentlyUsedPostLanguages.filter((lang) => {
|
||||
return lang !== language.value && languages.includes(lang);
|
||||
}),
|
||||
)
|
||||
.slice(0, maxLength),
|
||||
);
|
||||
|
@ -1268,9 +1250,7 @@ onMounted(() => {
|
|||
visibility.value = draft.data.visibility;
|
||||
localOnly.value = draft.data.localOnly;
|
||||
language.value = draft.data.lang;
|
||||
files.value = (draft.data.files || []).filter(
|
||||
(draftFile) => draftFile,
|
||||
);
|
||||
files.value = (draft.data.files || []).filter((draftFile) => draftFile);
|
||||
if (draft.data.poll) {
|
||||
poll.value = draft.data.poll;
|
||||
}
|
||||
|
@ -1298,6 +1278,14 @@ onMounted(() => {
|
|||
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(() => autosize.update(textareaEl.value));
|
||||
});
|
||||
|
|
|
@ -107,8 +107,7 @@ async function describe(file) {
|
|||
{
|
||||
done: (result) => {
|
||||
if (!result || result.canceled) return;
|
||||
const comment =
|
||||
result.result.length === 0 ? null : result.result;
|
||||
const comment = result.result.length === 0 ? null : result.result;
|
||||
os.api("drive/files/update", {
|
||||
fileId: file.id,
|
||||
comment,
|
||||
|
|
|
@ -45,7 +45,7 @@ const isRefreshing = ref(false);
|
|||
const pullDistance = ref(0);
|
||||
|
||||
let disabled = false;
|
||||
let supportPointerDesktop = false;
|
||||
const supportPointerDesktop = false;
|
||||
let startScreenY: number | null = null;
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
|
@ -102,8 +102,7 @@ function moveBySystem(to: number): Promise<void> {
|
|||
return;
|
||||
}
|
||||
const nextHeight =
|
||||
initialHeight -
|
||||
(overHeight / RELEASE_TRANSITION_DURATION) * time;
|
||||
initialHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
|
||||
if (pullDistance.value < nextHeight) return;
|
||||
pullDistance.value = nextHeight;
|
||||
}, 1);
|
||||
|
|
|
@ -94,17 +94,14 @@ const pushRegistrationInServer = ref<
|
|||
>();
|
||||
|
||||
function subscribe() {
|
||||
if (!registration.value || !supported.value || !instance.swPublickey)
|
||||
return;
|
||||
if (!registration.value || !supported.value || !instance.swPublickey) return;
|
||||
|
||||
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
|
||||
return promiseDialog(
|
||||
registration.value.pushManager
|
||||
.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(
|
||||
instance.swPublickey,
|
||||
),
|
||||
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
|
||||
})
|
||||
.then(
|
||||
async (subscription) => {
|
||||
|
@ -121,9 +118,7 @@ function subscribe() {
|
|||
// When subscribe failed
|
||||
// 通知が許可されていなかったとき
|
||||
if (err?.name === "NotAllowedError") {
|
||||
console.info(
|
||||
"User denied the notification permission request.",
|
||||
);
|
||||
console.info("User denied the notification permission request.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -171,9 +166,7 @@ function encode(buffer: ArrayBuffer | null) {
|
|||
*/
|
||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, "+")
|
||||
.replace(/_/g, "/");
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
|
|
@ -102,10 +102,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
| HTMLElement
|
||||
| null
|
||||
| undefined);
|
||||
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + el.offsetWidth / 2;
|
||||
|
@ -129,10 +126,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
| HTMLElement
|
||||
| null
|
||||
| undefined);
|
||||
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + el.offsetWidth / 2;
|
||||
|
@ -157,10 +151,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
| HTMLElement
|
||||
| null
|
||||
| undefined);
|
||||
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + el.offsetWidth / 2;
|
||||
|
@ -182,10 +173,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
| HTMLElement
|
||||
| null
|
||||
| undefined);
|
||||
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + el.offsetWidth / 2;
|
||||
|
@ -221,10 +209,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
| HTMLElement
|
||||
| null
|
||||
| undefined);
|
||||
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + el.offsetWidth / 2;
|
||||
|
|
|
@ -198,9 +198,7 @@ function openDescription(kind: "words" | "users" | "range"): void {
|
|||
};
|
||||
|
||||
popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkSimpleTextWindow.vue"),
|
||||
),
|
||||
defineAsyncComponent(() => import("@/components/MkSimpleTextWindow.vue")),
|
||||
{
|
||||
title: i18n.ts.help,
|
||||
description: descriptions[kind],
|
||||
|
|
|
@ -205,13 +205,11 @@ function queryKey() {
|
|||
.get({
|
||||
publicKey: {
|
||||
challenge: byteify(challengeData.value.challenge, "base64"),
|
||||
allowCredentials: challengeData.value.securityKeys.map(
|
||||
(key) => ({
|
||||
allowCredentials: challengeData.value.securityKeys.map((key) => ({
|
||||
id: byteify(key.id, "hex"),
|
||||
type: "public-key",
|
||||
transports: ["usb", "nfc", "ble", "internal"],
|
||||
}),
|
||||
),
|
||||
})),
|
||||
timeout: 60 * 1000,
|
||||
},
|
||||
})
|
||||
|
@ -226,9 +224,7 @@ function queryKey() {
|
|||
username: username.value,
|
||||
password: password.value,
|
||||
signature: hexify(credential.response.signature),
|
||||
authenticatorData: hexify(
|
||||
credential.response.authenticatorData,
|
||||
),
|
||||
authenticatorData: hexify(credential.response.authenticatorData),
|
||||
clientDataJSON: hexify(credential.response.clientDataJSON),
|
||||
credentialId: credential.id,
|
||||
challengeId: challengeData.value.challengeId,
|
||||
|
@ -277,9 +273,7 @@ function onSubmit() {
|
|||
"hcaptcha-response": hCaptchaResponse.value,
|
||||
"g-recaptcha-response": reCaptchaResponse.value,
|
||||
token:
|
||||
user.value && user.value.twoFactorEnabled
|
||||
? token.value
|
||||
: undefined,
|
||||
user.value && user.value.twoFactorEnabled ? token.value : undefined,
|
||||
})
|
||||
.then((res) => {
|
||||
emit("login", res);
|
||||
|
|
|
@ -79,8 +79,8 @@ const el = ref<HTMLElement>();
|
|||
const width = ref(0);
|
||||
const height = ref(0);
|
||||
const colors = ["#eb6f92", "#9ccfd8", "#f6c177", "#f6c177", "#f6c177"];
|
||||
let stop = false,
|
||||
ro: ResizeObserver | undefined;
|
||||
let stop = false;
|
||||
let ro: ResizeObserver | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
if (!reducedMotion()) {
|
||||
|
@ -104,9 +104,7 @@ onMounted(() => {
|
|||
};
|
||||
particles.value.push(particle);
|
||||
window.setTimeout(() => {
|
||||
particles.value = particles.value.filter(
|
||||
(x) => x.id !== particle.id,
|
||||
);
|
||||
particles.value = particles.value.filter((x) => x.id !== particle.id);
|
||||
}, particle.dur - 100);
|
||||
|
||||
window.setTimeout(
|
||||
|
|
|
@ -60,13 +60,10 @@ function star(ev?: MouseEvent): void {
|
|||
os.api("notes/reactions/create", {
|
||||
noteId: props.note.id,
|
||||
reaction:
|
||||
defaultStore.state.woozyMode === true
|
||||
? "🥴"
|
||||
: instance.defaultReaction,
|
||||
defaultStore.state.woozyMode === true ? "🥴" : instance.defaultReaction,
|
||||
});
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + el.offsetWidth / 2;
|
||||
|
|
|
@ -66,8 +66,7 @@ function toggleStar(ev?: MouseEvent): void {
|
|||
reaction: instance.defaultReaction,
|
||||
});
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + el.offsetWidth / 2;
|
||||
|
|
|
@ -32,14 +32,9 @@ export default defineComponent({
|
|||
role: "tab",
|
||||
key: option.key,
|
||||
"aria-selected":
|
||||
this.modelValue === option.props?.value
|
||||
? "true"
|
||||
: "false",
|
||||
this.modelValue === option.props?.value ? "true" : "false",
|
||||
onClick: () => {
|
||||
this.$emit(
|
||||
"update:modelValue",
|
||||
option.props?.value,
|
||||
);
|
||||
this.$emit("update:modelValue", option.props?.value);
|
||||
},
|
||||
},
|
||||
option.children,
|
||||
|
|
|
@ -25,18 +25,12 @@ const SAFE_FOR_HTML_ID = "abcdefghijklmnopqrstuvwxyz";
|
|||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const idForCanvas = Array.from(Array(16))
|
||||
.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("");
|
||||
const idForTags = Array.from(Array(16))
|
||||
.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("");
|
||||
const available = ref(false);
|
||||
|
|
|
@ -174,12 +174,12 @@ provide("inWindow", true);
|
|||
|
||||
const rootEl = ref<HTMLElement | null>();
|
||||
const showing = ref(true);
|
||||
let beforeClickedAt = 0,
|
||||
maximized = ref(false),
|
||||
unMaximizedTop = "",
|
||||
unMaximizedLeft = "",
|
||||
unMaximizedWidth = "",
|
||||
unMaximizedHeight = "";
|
||||
let beforeClickedAt = 0;
|
||||
const maximized = ref(false);
|
||||
let unMaximizedTop = "";
|
||||
let unMaximizedLeft = "";
|
||||
let unMaximizedWidth = "";
|
||||
let unMaximizedHeight = "";
|
||||
|
||||
function close() {
|
||||
showing.value = false;
|
||||
|
@ -203,9 +203,7 @@ function onContextmenu(ev: MouseEvent) {
|
|||
// 最前面へ移動
|
||||
function top() {
|
||||
if (rootEl.value) {
|
||||
rootEl.value.style.zIndex = os.claimZIndex(
|
||||
props.front ? "middle" : "low",
|
||||
);
|
||||
rootEl.value.style.zIndex = os.claimZIndex(props.front ? "middle" : "low");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,8 +280,8 @@ function onHeaderMousedown(evt: MouseEvent) {
|
|||
const windowHeight = main.offsetHeight;
|
||||
|
||||
function move(x: number, y: number) {
|
||||
let moveLeft = x - moveBaseX,
|
||||
moveTop = y - moveBaseY;
|
||||
let moveLeft = x - moveBaseX;
|
||||
let moveTop = y - moveBaseY;
|
||||
|
||||
// 下はみ出し
|
||||
if (moveTop + windowHeight > browserHeight)
|
||||
|
@ -310,13 +308,9 @@ function onHeaderMousedown(evt: MouseEvent) {
|
|||
// 動かした時
|
||||
dragListen((me) => {
|
||||
const x =
|
||||
me.touches && me.touches.length > 0
|
||||
? me.touches[0].clientX
|
||||
: me.clientX;
|
||||
me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
|
||||
const y =
|
||||
me.touches && me.touches.length > 0
|
||||
? me.touches[0].clientY
|
||||
: me.clientY;
|
||||
me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
|
||||
|
||||
move(x, y);
|
||||
});
|
||||
|
|
|
@ -146,14 +146,12 @@ useInterval(
|
|||
() => {
|
||||
if (prefixEl.value) {
|
||||
if (prefixEl.value.offsetWidth) {
|
||||
inputEl.value.style.paddingLeft =
|
||||
prefixEl.value.offsetWidth + "px";
|
||||
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + "px";
|
||||
}
|
||||
}
|
||||
if (suffixEl.value) {
|
||||
if (suffixEl.value.offsetWidth) {
|
||||
inputEl.value.style.paddingRight =
|
||||
suffixEl.value.offsetWidth + "px";
|
||||
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + "px";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -60,8 +60,7 @@ export default defineComponent({
|
|||
value: option.props?.value,
|
||||
disabled: option.props?.disabled,
|
||||
modelValue: this.value,
|
||||
"onUpdate:modelValue": (value) =>
|
||||
(this.value = value),
|
||||
"onUpdate:modelValue": (value) => (this.value = value),
|
||||
},
|
||||
option.children,
|
||||
),
|
||||
|
|
|
@ -120,14 +120,12 @@ useInterval(
|
|||
() => {
|
||||
if (prefixEl.value) {
|
||||
if (prefixEl.value.offsetWidth) {
|
||||
inputEl.value.style.paddingLeft =
|
||||
prefixEl.value.offsetWidth + "px";
|
||||
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + "px";
|
||||
}
|
||||
}
|
||||
if (suffixEl.value) {
|
||||
if (suffixEl.value.offsetWidth) {
|
||||
inputEl.value.style.paddingRight =
|
||||
suffixEl.value.offsetWidth + "px";
|
||||
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + "px";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -88,9 +88,7 @@ const choseAd = (): Ad | null => {
|
|||
return widgetAds;
|
||||
} else if (ads.length === 0) {
|
||||
if (lowPriorityAds.length !== 0) {
|
||||
return lowPriorityAds[
|
||||
Math.floor(Math.random() * lowPriorityAds.length)
|
||||
];
|
||||
return lowPriorityAds[Math.floor(Math.random() * lowPriorityAds.length)];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -24,10 +24,10 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
let ro: ResizeObserver,
|
||||
root = ref<HTMLElement>(),
|
||||
content = ref<HTMLElement>(),
|
||||
margin = ref(0);
|
||||
let ro: ResizeObserver;
|
||||
const root = ref<HTMLElement>();
|
||||
const content = ref<HTMLElement>();
|
||||
const margin = ref(0);
|
||||
const shouldSpacerMin = inject("shouldSpacerMin", false);
|
||||
|
||||
const adjust = (rect: { width: number; height: number }) => {
|
||||
|
|
|
@ -51,9 +51,7 @@ const el = ref();
|
|||
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkUrlPreviewPopup.vue"),
|
||||
),
|
||||
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
|
||||
{
|
||||
showing,
|
||||
url: props.url,
|
||||
|
|
|
@ -36,11 +36,9 @@ export default defineComponent({
|
|||
const note: Ref<Record<string, any> | null> = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
os.api("notes/show", { noteId: props.block.note }).then(
|
||||
(result) => {
|
||||
os.api("notes/show", { noteId: props.block.note }).then((result) => {
|
||||
note.value = result;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -63,10 +63,7 @@ export default defineComponent({
|
|||
const formData = new FormData();
|
||||
formData.append("file", blob);
|
||||
if (this.defaultStore.state.uploadFolder) {
|
||||
formData.append(
|
||||
"folderId",
|
||||
this.defaultStore.state.uploadFolder,
|
||||
);
|
||||
formData.append("folderId", this.defaultStore.state.uploadFolder);
|
||||
}
|
||||
|
||||
fetch(apiUrl + "/drive/files/create", {
|
||||
|
@ -87,9 +84,7 @@ export default defineComponent({
|
|||
},
|
||||
async post() {
|
||||
this.posting = true;
|
||||
const file = this.block.attachCanvasImage
|
||||
? await this.upload()
|
||||
: null;
|
||||
const file = this.block.attachCanvasImage ? await this.upload() : null;
|
||||
os.apiWithDialog("notes/create", {
|
||||
text: this.text === "" ? null : this.text,
|
||||
fileIds: file ? [file.id] : undefined,
|
||||
|
|
|
@ -93,8 +93,8 @@ export class Router extends EventEmitter<{
|
|||
}
|
||||
|
||||
public resolve(path: string): Resolved | null {
|
||||
let queryString: string | null = null,
|
||||
hash: string | null = null;
|
||||
let queryString: string | null = null;
|
||||
let hash: string | null = null;
|
||||
if (path[0] === "/") path = path.substring(1);
|
||||
if (path.includes("#")) {
|
||||
hash = path.substring(path.indexOf("#") + 1);
|
||||
|
|
|
@ -776,8 +776,8 @@ type AwaitType<T> = T extends Promise<infer U>
|
|||
: T extends (...args: any[]) => Promise<infer V>
|
||||
? V
|
||||
: T;
|
||||
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null,
|
||||
activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
|
||||
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
|
||||
let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
|
||||
export async function openEmojiPicker(
|
||||
src?: HTMLElement,
|
||||
opts,
|
||||
|
|
|
@ -94,10 +94,7 @@ export default defineComponent({
|
|||
|
||||
methods: {
|
||||
search() {
|
||||
if (
|
||||
(this.q === "" || this.q == null) &&
|
||||
this.selectedTags.size === 0
|
||||
) {
|
||||
if ((this.q === "" || this.q == null) && this.selectedTags.size === 0) {
|
||||
this.searchEmojis = null;
|
||||
return;
|
||||
}
|
||||
|
@ -105,17 +102,13 @@ export default defineComponent({
|
|||
if (this.selectedTags.size === 0) {
|
||||
this.searchEmojis = this.customEmojis.filter(
|
||||
(emoji) =>
|
||||
emoji.name.includes(this.q) ||
|
||||
emoji.aliases.includes(this.q),
|
||||
emoji.name.includes(this.q) || emoji.aliases.includes(this.q),
|
||||
);
|
||||
} else {
|
||||
this.searchEmojis = this.customEmojis.filter(
|
||||
(emoji) =>
|
||||
(emoji.name.includes(this.q) ||
|
||||
emoji.aliases.includes(this.q)) &&
|
||||
[...this.selectedTags].every((t) =>
|
||||
emoji.aliases.includes(t),
|
||||
),
|
||||
(emoji.name.includes(this.q) || emoji.aliases.includes(this.q)) &&
|
||||
[...this.selectedTags].every((t) => emoji.aliases.includes(t)),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -276,9 +276,7 @@ function easterEgg() {
|
|||
setTimeout(() => {
|
||||
if (iconClicks % 6 === 0) {
|
||||
iconSrc.value =
|
||||
instance.iconUrl ||
|
||||
instance.faviconUrl ||
|
||||
"/favicon.ico";
|
||||
instance.iconUrl || instance.faviconUrl || "/favicon.ico";
|
||||
} else {
|
||||
iconSrc.value = "/static-assets/woozy.png";
|
||||
}
|
||||
|
|
|
@ -254,9 +254,7 @@ const headerTabs = computed(() => [
|
|||
|
||||
definePageMetadata(
|
||||
computed(() => ({
|
||||
title: file.value
|
||||
? i18n.ts.file + ": " + file.value.name
|
||||
: i18n.ts.file,
|
||||
title: file.value ? i18n.ts.file + ": " + file.value.name : i18n.ts.file,
|
||||
icon: `${icon("ph-file")}`,
|
||||
})),
|
||||
);
|
||||
|
|
|
@ -169,12 +169,10 @@ onMounted(() => {
|
|||
if (tabEl && tabHighlightEl.value) {
|
||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||
const parentRect =
|
||||
tabEl.parentElement.getBoundingClientRect();
|
||||
const parentRect = tabEl.parentElement.getBoundingClientRect();
|
||||
const rect = tabEl.getBoundingClientRect();
|
||||
tabHighlightEl.value.style.width = rect.width + "px";
|
||||
tabHighlightEl.value.style.left =
|
||||
rect.left - parentRect.left + "px";
|
||||
tabHighlightEl.value.style.left = rect.left - parentRect.left + "px";
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -97,9 +97,7 @@ function remove(announcement) {
|
|||
text: i18n.t("removeAreYouSure", { x: announcement.title }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
announcements.value = announcements.value.filter(
|
||||
(x) => x !== announcement,
|
||||
);
|
||||
announcements.value = announcements.value.filter((x) => x !== announcement);
|
||||
os.api("admin/announcements/delete", announcement);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,9 +46,7 @@ import icon from "@/scripts/icon";
|
|||
const databasePromiseFactory = () =>
|
||||
os
|
||||
.api("admin/get-table-stats")
|
||||
.then((res) =>
|
||||
Object.entries(res).sort((a, b) => b[1].size - a[1].size),
|
||||
);
|
||||
.then((res) => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
|
|
|
@ -184,9 +184,7 @@ const remotePagination = {
|
|||
limit: 30,
|
||||
params: computed(() => ({
|
||||
query:
|
||||
queryRemote.value && queryRemote.value !== ""
|
||||
? queryRemote.value
|
||||
: null,
|
||||
queryRemote.value && queryRemote.value !== "" ? queryRemote.value : null,
|
||||
host: host.value && host.value !== "" ? host.value : null,
|
||||
})),
|
||||
};
|
||||
|
@ -203,9 +201,7 @@ const selectAll = () => {
|
|||
|
||||
const toggleSelect = (emoji) => {
|
||||
if (selectedEmojis.value.includes(emoji.id)) {
|
||||
selectedEmojis.value = selectedEmojis.value.filter(
|
||||
(x) => x !== emoji.id,
|
||||
);
|
||||
selectedEmojis.value = selectedEmojis.value.filter((x) => x !== emoji.id);
|
||||
} else {
|
||||
selectedEmojis.value.push(emoji.id);
|
||||
}
|
||||
|
@ -305,9 +301,7 @@ const menu = (ev: MouseEvent) => {
|
|||
icon: `${icon("ph-upload-simple")}`,
|
||||
text: i18n.ts.importZip,
|
||||
action: async () => {
|
||||
const file = await selectFile(
|
||||
ev.currentTarget ?? ev.target,
|
||||
);
|
||||
const file = await selectFile(ev.currentTarget ?? ev.target);
|
||||
os.api("admin/emoji/import-zip", {
|
||||
fileId: file.id,
|
||||
})
|
||||
|
@ -329,10 +323,7 @@ const menu = (ev: MouseEvent) => {
|
|||
icon: `${icon("ph-info")}`,
|
||||
text: i18n.ts.emojiPackCreator,
|
||||
action: () => {
|
||||
window.open(
|
||||
"https://firefish.dev/firefish/emoji-gen",
|
||||
"_blank",
|
||||
);
|
||||
window.open("https://firefish.dev/firefish/emoji-gen", "_blank");
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -103,9 +103,7 @@ const pagination = {
|
|||
userId: userId.value && userId.value !== "" ? userId.value : null,
|
||||
origin: origin.value,
|
||||
hostname:
|
||||
searchHost.value && searchHost.value !== ""
|
||||
? searchHost.value
|
||||
: null,
|
||||
searchHost.value && searchHost.value !== "" ? searchHost.value : null,
|
||||
})),
|
||||
};
|
||||
|
||||
|
|
|
@ -46,8 +46,7 @@ async function init() {
|
|||
|
||||
function save() {
|
||||
os.apiWithDialog("admin/update-meta", {
|
||||
hiddenTags:
|
||||
hiddenTags.value.split("\n").map((h: string) => h.trim()) || [],
|
||||
hiddenTags: hiddenTags.value.split("\n").map((h: string) => h.trim()) || [],
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
|
|
@ -221,31 +221,25 @@ const menuDef = computed(() => [
|
|||
icon: `${icon("ph-gear-six")}`,
|
||||
text: i18n.ts.general,
|
||||
to: "/admin/settings",
|
||||
active:
|
||||
currentPage.value?.route.name === "settings",
|
||||
active: currentPage.value?.route.name === "settings",
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-envelope-simple-open")}`,
|
||||
text: i18n.ts.emailServer,
|
||||
to: "/admin/email-settings",
|
||||
active:
|
||||
currentPage.value?.route.name ===
|
||||
"email-settings",
|
||||
active: currentPage.value?.route.name === "email-settings",
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-cloud")}`,
|
||||
text: i18n.ts.objectStorage,
|
||||
to: "/admin/object-storage",
|
||||
active:
|
||||
currentPage.value?.route.name ===
|
||||
"object-storage",
|
||||
active: currentPage.value?.route.name === "object-storage",
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-lock")}`,
|
||||
text: i18n.ts.security,
|
||||
to: "/admin/security",
|
||||
active:
|
||||
currentPage.value?.route.name === "security",
|
||||
active: currentPage.value?.route.name === "security",
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-arrows-merge")}`,
|
||||
|
@ -257,38 +251,31 @@ const menuDef = computed(() => [
|
|||
icon: `${icon("ph-prohibit")}`,
|
||||
text: i18n.ts.instanceBlocking,
|
||||
to: "/admin/instance-block",
|
||||
active:
|
||||
currentPage.value?.route.name ===
|
||||
"instance-block",
|
||||
active: currentPage.value?.route.name === "instance-block",
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-hash")}`,
|
||||
text: i18n.ts.hiddenTags,
|
||||
to: "/admin/hashtags",
|
||||
active:
|
||||
currentPage.value?.route.name === "hashtags",
|
||||
active: currentPage.value?.route.name === "hashtags",
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-ghost")}`,
|
||||
text: i18n.ts.proxyAccount,
|
||||
to: "/admin/proxy-account",
|
||||
active:
|
||||
currentPage.value?.route.name ===
|
||||
"proxy-account",
|
||||
active: currentPage.value?.route.name === "proxy-account",
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-database")}`,
|
||||
text: i18n.ts.database,
|
||||
to: "/admin/database",
|
||||
active:
|
||||
currentPage.value?.route.name === "database",
|
||||
active: currentPage.value?.route.name === "database",
|
||||
},
|
||||
{
|
||||
icon: `${icon("ph-flask")}`,
|
||||
text: i18n.ts._experiments.title,
|
||||
to: "/admin/experiments",
|
||||
active:
|
||||
currentPage.value?.route.name === "experiments",
|
||||
active: currentPage.value?.route.name === "experiments",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -70,8 +70,7 @@ async function init() {
|
|||
function save() {
|
||||
os.apiWithDialog("admin/update-meta", {
|
||||
blockedHosts: blockedHosts.value.split("\n").map((h) => h.trim()) || [],
|
||||
silencedHosts:
|
||||
silencedHosts.value.split("\n").map((h) => h.trim()) || [],
|
||||
silencedHosts: silencedHosts.value.split("\n").map((h) => h.trim()) || [],
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
|
|
@ -530,7 +530,8 @@ function isValidHttpUrl(src: string) {
|
|||
function parseMoreUrls(src: string): { name: string; url: string }[] {
|
||||
const toReturn: { name: string; url: string }[] = [];
|
||||
const pattern = /"(.+)"\s*:\s*(http.+)/;
|
||||
src.trim()
|
||||
src
|
||||
.trim()
|
||||
.split("\n")
|
||||
.forEach((line) => {
|
||||
const match = pattern.exec(line);
|
||||
|
@ -590,9 +591,7 @@ async function init() {
|
|||
defaultReaction.value = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
|
||||
? meta.defaultReaction
|
||||
: "custom";
|
||||
defaultReactionCustom.value = ["⭐", "👍", "❤️"].includes(
|
||||
meta.defaultReaction,
|
||||
)
|
||||
defaultReactionCustom.value = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
|
||||
? ""
|
||||
: meta.defaultReaction;
|
||||
enableServerMachineStats.value = meta.enableServerMachineStats;
|
||||
|
|
|
@ -72,11 +72,9 @@ onMounted(() => {
|
|||
fetching.value = false;
|
||||
|
||||
if (session.value.app.isAuthorized) {
|
||||
os.api("auth/accept", { token: session.value.token }).then(
|
||||
() => {
|
||||
os.api("auth/accept", { token: session.value.token }).then(() => {
|
||||
accepted();
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
state.value = "waiting";
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref } from "vue";
|
||||
import * as os from "@/os";
|
||||
import { useRouter } from "@/router";
|
||||
import { userPage } from "@/filters/user";
|
||||
import { notePage } from "@/filters/note";
|
||||
import { onMounted, ref, defineAsyncComponent } from "vue";
|
||||
const XNotFound = defineAsyncComponent(() => import("./not-found.vue"));
|
||||
|
||||
const err = ref(false);
|
||||
|
|
|
@ -75,9 +75,7 @@ const headerActions = computed(() =>
|
|||
icon: `${icon("ph-pencil")}`,
|
||||
text: i18n.ts.toEdit,
|
||||
handler: async (): Promise<void> => {
|
||||
const { canceled, result } = await os.form(
|
||||
clip.value.name,
|
||||
{
|
||||
const { canceled, result } = await os.form(clip.value.name, {
|
||||
name: {
|
||||
type: "string",
|
||||
label: i18n.ts.name,
|
||||
|
@ -95,8 +93,7 @@ const headerActions = computed(() =>
|
|||
label: i18n.ts.public,
|
||||
default: clip.value.isPublic,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog("clips/update", {
|
||||
|
|
|
@ -37,14 +37,12 @@ function menu(ev) {
|
|||
text: i18n.ts.license,
|
||||
icon: `${icon("ph-info")}`,
|
||||
action: () => {
|
||||
os.apiGet("emoji", { name: props.emoji.name }).then(
|
||||
(res) => {
|
||||
os.apiGet("emoji", { name: props.emoji.name }).then((res) => {
|
||||
os.alert({
|
||||
type: "info",
|
||||
text: `${res.license || i18n.ts.notSet}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -182,8 +182,7 @@ function onMessage(message): void {
|
|||
!(
|
||||
(m.recipientId === message.recipientId &&
|
||||
m.userId === message.userId) ||
|
||||
(m.recipientId === message.userId &&
|
||||
m.userId === message.recipientId)
|
||||
(m.recipientId === message.userId && m.userId === message.recipientId)
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -267,8 +266,7 @@ onMounted(() => {
|
|||
const _messages = userMessages.concat(groupMessages);
|
||||
_messages.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() -
|
||||
new Date(a.createdAt).getTime(),
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
messages.value = _messages;
|
||||
},
|
||||
|
|
|
@ -90,8 +90,7 @@ const draftKey = computed(() =>
|
|||
props.user ? "user:" + props.user.id : "group:" + props.group?.id,
|
||||
);
|
||||
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);
|
||||
|
|
|
@ -280,9 +280,7 @@ function onRead(x) {
|
|||
if (!Array.isArray(x)) x = [x];
|
||||
for (const id of x) {
|
||||
if (pagingComponent.value.items.some((y) => y.id === id)) {
|
||||
const exist = pagingComponent.value.items
|
||||
.map((y) => y.id)
|
||||
.indexOf(id);
|
||||
const exist = pagingComponent.value.items.map((y) => y.id).indexOf(id);
|
||||
pagingComponent.value.items[exist] = {
|
||||
...pagingComponent.value.items[exist],
|
||||
isRead: true,
|
||||
|
@ -292,15 +290,10 @@ function onRead(x) {
|
|||
} else if (group.value) {
|
||||
for (const id of x.ids) {
|
||||
if (pagingComponent.value.items.some((y) => y.id === id)) {
|
||||
const exist = pagingComponent.value.items
|
||||
.map((y) => y.id)
|
||||
.indexOf(id);
|
||||
const exist = pagingComponent.value.items.map((y) => y.id).indexOf(id);
|
||||
pagingComponent.value.items[exist] = {
|
||||
...pagingComponent.value.items[exist],
|
||||
reads: [
|
||||
...pagingComponent.value.items[exist].reads,
|
||||
x.userId,
|
||||
],
|
||||
reads: [...pagingComponent.value.items[exist].reads, x.userId],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,8 +72,8 @@ const headerTabs = computed(() => []);
|
|||
|
||||
const list = ref<typeof MkPagination | null>(null);
|
||||
|
||||
let isCached = false,
|
||||
refreshTimer: number | null = null;
|
||||
let isCached = false;
|
||||
let refreshTimer: number | null = null;
|
||||
|
||||
const refresh = () => {
|
||||
if (isCached) {
|
||||
|
|
|
@ -175,20 +175,15 @@ definePageMetadata(
|
|||
appearNote.value
|
||||
? {
|
||||
title: i18n.t("noteOf", {
|
||||
user:
|
||||
appearNote.value.user.name ||
|
||||
appearNote.value.user.username,
|
||||
user: appearNote.value.user.name || appearNote.value.user.username,
|
||||
}),
|
||||
subtitle: new Date(
|
||||
appearNote.value.createdAt,
|
||||
).toLocaleString(),
|
||||
subtitle: new Date(appearNote.value.createdAt).toLocaleString(),
|
||||
avatar: appearNote.value.user,
|
||||
path: `/notes/${appearNote.value.id}`,
|
||||
share: {
|
||||
title: i18n.t("noteOf", {
|
||||
user:
|
||||
appearNote.value.user.name ||
|
||||
appearNote.value.user.username,
|
||||
appearNote.value.user.name || appearNote.value.user.username,
|
||||
}),
|
||||
text: appearNote.value.text,
|
||||
},
|
||||
|
|
|
@ -164,9 +164,7 @@ export default defineComponent({
|
|||
components: {
|
||||
XContainer,
|
||||
MkTextarea,
|
||||
XV: defineAsyncComponent(
|
||||
() => import("./page-editor.script-block.vue"),
|
||||
),
|
||||
XV: defineAsyncComponent(() => import("./page-editor.script-block.vue")),
|
||||
},
|
||||
|
||||
inject: ["getScriptBlockList"],
|
||||
|
@ -228,9 +226,7 @@ export default defineComponent({
|
|||
watch: {
|
||||
slots: {
|
||||
handler() {
|
||||
this.modelValue.value.slots = this.slots
|
||||
.split("\n")
|
||||
.map((x) => ({
|
||||
this.modelValue.value.slots = this.slots.split("\n").map((x) => ({
|
||||
name: x,
|
||||
type: null,
|
||||
}));
|
||||
|
@ -243,9 +239,7 @@ export default defineComponent({
|
|||
if (this.modelValue.value == null) this.modelValue.value = null;
|
||||
|
||||
if (this.modelValue.value && this.modelValue.value.slots)
|
||||
this.slots = this.modelValue.value.slots
|
||||
.map((x) => x.name)
|
||||
.join("\n");
|
||||
this.slots = this.modelValue.value.slots.map((x) => x.name).join("\n");
|
||||
|
||||
this.$watch(
|
||||
() => this.modelValue.type,
|
||||
|
@ -261,10 +255,7 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.modelValue.type &&
|
||||
this.modelValue.type.startsWith("fn:")
|
||||
) {
|
||||
if (this.modelValue.type && this.modelValue.type.startsWith("fn:")) {
|
||||
const fnName = this.modelValue.type.split(":")[1];
|
||||
const fn = this.hpml.getVarByName(fnName);
|
||||
|
||||
|
@ -280,27 +271,17 @@ export default defineComponent({
|
|||
if (isLiteralValue(this.modelValue)) return;
|
||||
|
||||
const empties = [];
|
||||
for (
|
||||
let i = 0;
|
||||
i < funcDefs[this.modelValue.type].in.length;
|
||||
i++
|
||||
) {
|
||||
for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
|
||||
const id = uuid();
|
||||
empties.push({ id, type: null });
|
||||
}
|
||||
this.modelValue.args = empties;
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < funcDefs[this.modelValue.type].in.length;
|
||||
i++
|
||||
) {
|
||||
for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
|
||||
const inType = funcDefs[this.modelValue.type].in[i];
|
||||
if (typeof inType !== "number") {
|
||||
if (inType === "number")
|
||||
this.modelValue.args[i].type = "number";
|
||||
if (inType === "string")
|
||||
this.modelValue.args[i].type = "text";
|
||||
if (inType === "number") this.modelValue.args[i].type = "number";
|
||||
if (inType === "string") this.modelValue.args[i].type = "text";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -47,7 +47,6 @@
|
|||
v-tooltip="i18n.ts._pages.viewSource"
|
||||
:to="`/@${username}/pages/${pageName}/view-source`"
|
||||
class="menu _button"
|
||||
style="transform: translateY(2px)"
|
||||
><i :class="icon('ph-code')"
|
||||
/></MkA>
|
||||
<template
|
||||
|
@ -59,7 +58,6 @@
|
|||
v-tooltip="i18n.ts._pages.editPage"
|
||||
class="menu _button"
|
||||
:to="`/pages/edit/${page.id}`"
|
||||
style="transform: translateY(2px)"
|
||||
><i :class="icon('ph-pencil')"
|
||||
/></MkA>
|
||||
<button
|
||||
|
@ -215,7 +213,7 @@ import { definePageMetadata } from "@/scripts/page-metadata";
|
|||
import { shareAvailable } from "@/scripts/share-available";
|
||||
import { defaultStore } from "@/store";
|
||||
import icon from "@/scripts/icon";
|
||||
import { isSignedIn } from "@/me";
|
||||
import { me, isSignedIn } from "@/me";
|
||||
|
||||
const props = defineProps<{
|
||||
pageName: string;
|
||||
|
@ -381,6 +379,7 @@ definePageMetadata(
|
|||
color: #fff;
|
||||
text-shadow: 0 0 8px var(--shadow);
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
> .koudoku {
|
||||
|
|
|
@ -64,9 +64,7 @@ function fetchKeys() {
|
|||
os.api("i/registry/keys-with-type", {
|
||||
scope: scope.value,
|
||||
}).then((res) => {
|
||||
keys.value = Object.entries(res).sort((a, b) =>
|
||||
a[0].localeCompare(b[0]),
|
||||
);
|
||||
keys.value = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -56,9 +56,7 @@ async function save() {
|
|||
onMounted(() => {
|
||||
if (props.token == null) {
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkForgotPassword.vue"),
|
||||
),
|
||||
defineAsyncComponent(() => import("@/components/MkForgotPassword.vue")),
|
||||
{},
|
||||
{},
|
||||
"closed",
|
||||
|
|
|
@ -89,10 +89,7 @@ async function run() {
|
|||
out: (value) => {
|
||||
logs.value.push({
|
||||
id: Math.random(),
|
||||
text:
|
||||
value.type === "str"
|
||||
? value.value
|
||||
: utils.valToString(value),
|
||||
text: value.type === "str" ? value.value : utils.valToString(value),
|
||||
print: true,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -78,8 +78,7 @@ const notesPagination = {
|
|||
host: props.host == null ? undefined : getHost(props.host),
|
||||
sinceDate:
|
||||
props.since == null ? undefined : getUnixTime(props.since, false),
|
||||
untilDate:
|
||||
props.until == null ? undefined : getUnixTime(props.until, true),
|
||||
untilDate: props.until == null ? undefined : getUnixTime(props.until, true),
|
||||
withFiles: props.withFiles === "1",
|
||||
searchCwAndAlt: props.searchCwAndAlt === "1",
|
||||
channelId: props.channel,
|
||||
|
|
|
@ -107,8 +107,7 @@ async function install() {
|
|||
return;
|
||||
}
|
||||
|
||||
const { name, version, author, description, permissions, config } =
|
||||
metadata;
|
||||
const { name, version, author, description, permissions, config } = metadata;
|
||||
if (name == null || version == null || author == null) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
|
@ -123,29 +122,22 @@ async function install() {
|
|||
: await new Promise((res, rej) => {
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
"@/components/MkTokenGenerateWindow.vue"
|
||||
),
|
||||
() => import("@/components/MkTokenGenerateWindow.vue"),
|
||||
),
|
||||
{
|
||||
title: i18n.ts.tokenRequested,
|
||||
information:
|
||||
i18n.ts.pluginTokenRequestedDescription,
|
||||
information: i18n.ts.pluginTokenRequestedDescription,
|
||||
initialName: name,
|
||||
initialPermissions: permissions,
|
||||
},
|
||||
{
|
||||
done: async (result) => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api(
|
||||
"miauth/gen-token",
|
||||
{
|
||||
const { token } = await os.api("miauth/gen-token", {
|
||||
session: null,
|
||||
name,
|
||||
permission: permissions,
|
||||
},
|
||||
);
|
||||
});
|
||||
res(token);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -196,10 +196,7 @@ function validate(profile: unknown): void {
|
|||
|
||||
// Check if createdAt and updatedAt is Date
|
||||
// https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date
|
||||
if (
|
||||
!profile.createdAt ||
|
||||
Number.isNaN(new Date(profile.createdAt).getTime())
|
||||
)
|
||||
if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt).getTime()))
|
||||
throw new Error("createdAt is falsy or not Date");
|
||||
if (profile.updatedAt) {
|
||||
if (Number.isNaN(new Date(profile.updatedAt).getTime())) {
|
||||
|
@ -228,10 +225,7 @@ function getSettings(): Profile["settings"] {
|
|||
hot,
|
||||
cold,
|
||||
fontSize: localStorage.getItem("fontSize"),
|
||||
useSystemFont: localStorage.getItem("useSystemFont") as
|
||||
| "t"
|
||||
| "f"
|
||||
| null,
|
||||
useSystemFont: localStorage.getItem("useSystemFont") as "t" | "f" | null,
|
||||
wallpaper: localStorage.getItem("wallpaper"),
|
||||
};
|
||||
}
|
||||
|
@ -468,12 +462,9 @@ function menu(ev: MouseEvent, profileId: string) {
|
|||
text: i18n.ts.download,
|
||||
icon: `${icon("ph-download-simple")}`,
|
||||
href: URL.createObjectURL(
|
||||
new Blob(
|
||||
[JSON.stringify(profiles.value[profileId], null, 2)],
|
||||
{
|
||||
new Blob([JSON.stringify(profiles.value[profileId], null, 2)], {
|
||||
type: "application/json",
|
||||
},
|
||||
),
|
||||
}),
|
||||
),
|
||||
download: `${profiles.value[profileId].name}.json`,
|
||||
},
|
||||
|
@ -502,9 +493,7 @@ function menu(ev: MouseEvent, profileId: string) {
|
|||
|
||||
onMounted(() => {
|
||||
// streamingのuser storage updateイベントを監視して更新
|
||||
connection?.on(
|
||||
"registryUpdated",
|
||||
({ scope: recievedScope, key, value }) => {
|
||||
connection?.on("registryUpdated", ({ scope: recievedScope, key, value }) => {
|
||||
if (
|
||||
!recievedScope ||
|
||||
recievedScope.length !== scope.length ||
|
||||
|
@ -514,8 +503,7 @@ onMounted(() => {
|
|||
if (!profiles.value) return;
|
||||
|
||||
profiles.value[key] = value;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
@ -213,9 +213,7 @@ while (fields.length < 4) {
|
|||
|
||||
function saveFields() {
|
||||
os.apiWithDialog("i/update", {
|
||||
fields: fields.filter(
|
||||
(field) => field.name !== "" && field.value !== "",
|
||||
),
|
||||
fields: fields.filter((field) => field.name !== "" && field.value !== ""),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -179,9 +179,7 @@ function remove(reaction, ev: MouseEvent) {
|
|||
{
|
||||
text: i18n.ts.remove,
|
||||
action: () => {
|
||||
reactions.value = reactions.value.filter(
|
||||
(x) => x !== reaction,
|
||||
);
|
||||
reactions.value = reactions.value.filter((x) => x !== reaction);
|
||||
save();
|
||||
},
|
||||
},
|
||||
|
@ -192,9 +190,7 @@ function remove(reaction, ev: MouseEvent) {
|
|||
|
||||
function preview(ev: MouseEvent) {
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkEmojiPickerDialog.vue"),
|
||||
),
|
||||
defineAsyncComponent(() => import("@/components/MkEmojiPickerDialog.vue")),
|
||||
{
|
||||
asReactionPicker: true,
|
||||
src: ev.currentTarget ?? ev.target,
|
||||
|
|
|
@ -71,13 +71,11 @@ const pagination = {
|
|||
};
|
||||
|
||||
async function change() {
|
||||
const { canceled: canceled1, result: currentPassword } = await os.inputText(
|
||||
{
|
||||
const { canceled: canceled1, result: currentPassword } = await os.inputText({
|
||||
title: i18n.ts.currentPassword,
|
||||
type: "password",
|
||||
autocomplete: "current-password",
|
||||
},
|
||||
);
|
||||
});
|
||||
if (canceled1) return;
|
||||
|
||||
const { canceled: canceled2, result: newPassword } = await os.inputText({
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue