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

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

View File

@ -1,7 +1,5 @@
ifneq (dev,$(wildcard config.env))
include ./dev/config.env
export
endif
include ./dev/config.env
export
.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:

View File

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

View File

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

View File

@ -5,6 +5,15 @@ Critical security updates are indicated by the :warning: icon.
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
- 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

View File

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

View File

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

View File

@ -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: Автоматично откриване на езика и показване на бутон за превеждане
за публикации на чужди езици

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
import config from "@/config/index.js";
import { 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 {

View File

@ -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");
}
if (finalUrl === object.id) return object;
if (new URL(finalUrl).host !== new URL(object.id).host) {
throw new Error("Object ID host doesn't match final url host");
}
const finalRes = await apGet(object.id, this.user);
if (finalRes.finalUrl !== finalRes.content.id)
throw new Error(
"Object ID still doesn't match final URL after second fetch attempt",
);
return finalRes.content;
}
private resolveLocal(url: string): Promise<IObject> {
private async resolveLocal(url: string): Promise<IObject> {
const parsed = parseUri(url);
if (!parsed.local) throw new Error("resolveLocal: not local");
switch (parsed.type) {
case "notes":
return Notes.findOneByOrFail({ id: parsed.id }).then((note) => {
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);
}
});
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`);
}
}
}

View File

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

View File

@ -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": "^_"
}
]
}
}

View File

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

View File

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

View File

@ -166,21 +166,21 @@ const texts = computed(() => {
return angles;
});
let enabled = true,
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;
let enabled = true;
const majorGraduationColor = ref<string>();
// let minorGraduationColor = $ref<string>();
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();

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -113,7 +113,7 @@ const url =
props.raw || defaultStore.state.loadRawImages
? props.media.url
: defaultStore.state.disableShowingAnimatedImages &&
props.media.type.startsWith("image")
props.media.type.startsWith("image")
? getStaticImageUrl(props.media.thumbnailUrl)
: props.media.thumbnailUrl;
@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -205,13 +205,11 @@ function queryKey() {
.get({
publicKey: {
challenge: byteify(challengeData.value.challenge, "base64"),
allowCredentials: challengeData.value.securityKeys.map(
(key) => ({
id: byteify(key.id, "hex"),
type: "public-key",
transports: ["usb", "nfc", "ble", "internal"],
}),
),
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {
note.value = result;
},
);
os.api("notes/show", { noteId: props.block.note }).then((result) => {
note.value = result;
});
});
return {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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(() => []);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.alert({
type: "info",
text: `${res.license || i18n.ts.notSet}`,
});
},
);
os.apiGet("emoji", { name: props.emoji.name }).then((res) => {
os.alert({
type: "info",
text: `${res.license || i18n.ts.notSet}`,
});
});
},
},
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,12 +226,10 @@ export default defineComponent({
watch: {
slots: {
handler() {
this.modelValue.value.slots = this.slots
.split("\n")
.map((x) => ({
name: x,
type: null,
}));
this.modelValue.value.slots = this.slots.split("\n").map((x) => ({
name: x,
type: null,
}));
},
deep: true,
},
@ -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";
}
}
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",
{
session: null,
name,
permission: permissions,
},
);
const { token } = await os.api("miauth/gen-token", {
session: null,
name,
permission: permissions,
});
res(token);
},
},

View File

@ -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)],
{
type: "application/json",
},
),
new Blob([JSON.stringify(profiles.value[profileId], null, 2)], {
type: "application/json",
}),
),
download: `${profiles.value[profileId].name}.json`,
},
@ -502,20 +493,17 @@ function menu(ev: MouseEvent, profileId: string) {
onMounted(() => {
// streaminguser storage update
connection?.on(
"registryUpdated",
({ scope: recievedScope, key, value }) => {
if (
!recievedScope ||
recievedScope.length !== scope.length ||
recievedScope[0] !== scope[0]
)
return;
if (!profiles.value) return;
connection?.on("registryUpdated", ({ scope: recievedScope, key, value }) => {
if (
!recievedScope ||
recievedScope.length !== scope.length ||
recievedScope[0] !== scope[0]
)
return;
if (!profiles.value) return;
profiles.value[key] = value;
},
);
profiles.value[key] = value;
});
});
onUnmounted(() => {

View File

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

View File

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

View File

@ -71,13 +71,11 @@ const pagination = {
};
async function change() {
const { canceled: canceled1, result: currentPassword } = await os.inputText(
{
title: i18n.ts.currentPassword,
type: "password",
autocomplete: "current-password",
},
);
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