Merge remote-branch 'upstream/develop'

This commit is contained in:
NoriDev 2023-02-09 03:13:31 +09:00
commit 6838fe4ed5
27 changed files with 261 additions and 145 deletions

View file

@ -8,6 +8,30 @@
You should also include the user name that made the change.
-->
## 13.5.2 (2023/02/08)
### Changes
- Revert: perf(client): do not render custom emojis in user names
### Bugfixes
- Client: register_note_view_interruptor not working
- Client: ログイントークンの再生成が出来ない
## 13.5.0 (2023/02/08)
### Changes
- perf(client): do not render custom emojis in user names
### Improvements
- Client: disableShowingAnimatedImagesのデフォルト値をprefers-reduced-motionにする
- enhance(client): tweak medialist style
### Bugfixes
- fix docker health check
- Client: MkEmojiPickerでもChromeで検索ダイアログで変換確定するとそのまま検索されてしまうのを修正
- fix(mfm): default degree not used in rotate
- fix(server): validate urls from ap to improve security
## 13.4.0 (2023/02/05)
### Improvements

View file

@ -8,9 +8,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -yqq --no-install-recommends \
build-essential wget ca-certificates \
&& wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq \
&& chmod +x /usr/bin/yq
build-essential
RUN corepack enable
@ -44,7 +42,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg tini \
ffmpeg tini curl \
&& corepack enable \
&& groupadd -g "${GID}" misskey \
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
@ -54,7 +52,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
USER misskey
WORKDIR /misskey
COPY --from=builder /usr/bin/yq /usr/bin/yq
COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=builder /misskey/built ./built
COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules

View file

@ -1,4 +1,4 @@
#!/bin/bash
PORT=$(yq '.port' /cherrypick/.config/default.yml)
PORT=$(grep '^port:' /cherrypick/.config/default.yml | awk 'NR==1{print $2; exit}')
curl -s -S -o /dev/null "http://localhost:${PORT}"

View file

@ -1044,7 +1044,7 @@ _achievements:
flavor: "Grazie per aver usato CherryPick!"
_noteClipped1:
title: "Devo clippare!"
description: "Ho raccolto in Clip la prima Nota"
description: "Hai raccolto la tua prima Nota in una Clip"
_noteFavorited1:
title: "Guarda le stelle"
description: "Aggiungi una Nota ai preferiti per la prima volta"
@ -1080,7 +1080,7 @@ _achievements:
title: "Follow me!"
description: "Hai ottenuto 10 profili Follower"
_followers50:
title: "Follower a frotte"
title: "Un gregge di Follower"
description: "Hai ottenuto 50 Follower"
_followers100:
title: "Popolare"
@ -1108,7 +1108,7 @@ _achievements:
title: "Caccia al tesoro"
description: "Hai trovato un tesoro nascosto"
_client30min:
title: "Piccola pausa"
title: "Piccola grande pausa"
description: "Hai passato più di 30 minuti su CherryPick"
_noteDeletedWithin1min:
title: "Ooops!"
@ -1134,7 +1134,7 @@ _achievements:
title: "Hello, world!"
description: "Hai scritto «Hello world» nel blocco appunti"
_open3windows:
title: "Finestrato"
title: "Apri le finestre!"
description: "Hai aperto almeno 3 finestre contemporaneamente"
_driveFolderCircularReference:
title: "Riferimento circolare"
@ -1170,7 +1170,7 @@ _achievements:
_cookieClicked:
title: "Clicca il biscotto"
description: "Hai giocato a cliccare il cookie"
flavor: "Hai autorizzato i cookie?"
flavor: "È il sito giusto?"
_brainDiver:
title: "Brain Diver"
description: "Pubblica un link a Brain Diver"
@ -1195,6 +1195,9 @@ _role:
baseRole: "Ruolo di base"
useBaseValue: "Eredita dal ruolo base"
chooseRoleToAssign: "Seleziona il ruolo da assegnare"
iconUrl: "URL dell'icona"
asBadge: "Mostra come badge"
descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo."
canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo"
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
priority: "Priorità"

View file

@ -1,2 +1,72 @@
---
_lang_: "ພາສາລາວ"
headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍຫມາຍເຫດ"
introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນແຫຼ່ງເປີດ, ການບໍລິການ microblogging ກະຈາຍ\nສ້າງ \"ບັນທຶກ\" ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nດ້ວຍ \"ປະຕິກິລິຍາ\", ທ່ານຍັງສາມາດສະແດງຄວາມຮູ້ສຶກຂອງທ່ານຢ່າງໄວວາກ່ຽວກັບບັນທຶກຂອງທຸກໆຄົນ 👍\nມາສຳຫຼວດໂລກໃໝ່! 🚀"
poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. <b>Misskey</b> (ເອີ້ນວ່າ \"Misskey instance\")"
monthAndDay: "{ເດືອນ}/{ມື້}"
search: "ຄົ້ນຫາ"
notifications: "ການແຈ້ງເຕືອນ"
username: "ຊື່ຜູ້ໃຊ້"
password: "ລະຫັດຜ່ານ"
forgotPassword: "ລືມລະຫັດຜ່ານ"
fetchingAsApObject: "ກຳລັງດຶງຂໍ້ມູນຈາກ fediverse..."
ok: "ຕົກ​ລົງ"
gotIt: "ເຂົ້າໃຈແລ້ວ!"
cancel: "ຍົກເລີກ"
noThankYou: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້"
enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້"
renotedBy: "Renoted ໂດຍ {ຜູ້ໃຊ້}"
noNotes: "ບໍ່ມີຫມາຍເຫດ"
noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ"
instance: "ອີນສະແຕນ"
settings: "ກຳນົດຄ່າ"
basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ"
otherSettings: "ການຕັ້ງຄ່າອື່ນໆ"
openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ"
profile: "ໂພຼຟາຍ"
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ"
login: "ເຂົ້າ​ສູ່​ລະ​ບົບ"
loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..."
logout: "ອອກ​ຈາກ​ລະ​ບົບ"
signup: "ລົງ​ທະ​ບຽນ"
uploading: "ການອັບໂຫຼດ..."
save: "ບັນທຶກ"
users: "ຜູ້ໃຊ້ຕ່າງໆ"
addUser: "ເພີ່ມຜູ້ໃຊ້"
favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ"
favorites: "ລາຍການທີ່ມັກ"
unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ"
favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ"
alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ."
cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້."
pin: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌"
unpin: "ຖອດປັກໝຸດອອກຈາກໂປຣໄຟລ໌"
copyContent: "ຄັດລອກເນື້ອຫາ"
copyLink: "ສຳເນົາລິ້ງ"
delete: "ລຶບ"
deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​"
deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ"
addToList: "ເພີ່ມໃສ່ລາຍຊື່"
sendMessage: "ສົ່ງຂໍ້ຄວາມ"
pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌"
instances: "ອີນສະແຕນ"
remove: "ລຶບ"
smtpUser: "ຊື່ຜູ້ໃຊ້"
smtpPass: "ລະຫັດຜ່ານ"
user: "ຜູ້ໃຊ້ຕ່າງໆ"
searchByGoogle: "ຄົ້ນຫາ"
_mfm:
search: "ຄົ້ນຫາ"
_sfx:
notification: "ການແຈ້ງເຕືອນ"
_widgets:
profile: "ໂພຼຟາຍ"
notifications: "ການແຈ້ງເຕືອນ"
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
_profile:
username: "ຊື່ຜູ້ໃຊ້"
_deck:
_columns:
notifications: "ການແຈ້ງເຕືອນ"
tl: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"

View file

@ -1147,7 +1147,7 @@ _achievements:
description: "คุณได้คลิกที่นี่"
_justPlainLucky:
title: "แค่ลัคกี้ธรรมดา"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที"
_setNameToSyuilo:
title: "พระเจ้าคอมเพล็กซ์"
description: "ตั้งชื่อของคุณเป็น \"syuilo\""
@ -1182,7 +1182,7 @@ _role:
description: "คำอธิบายบทบาท"
permission: "สิทธิ์ตามบทบาท"
descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ"
assignTarget: "กำหนดเป้าหมาย"
assignTarget: "มอบหมาย"
descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง"
manual: "ปรับเอง"
conditional: "มีเงื่อนไข"

View file

@ -1382,8 +1382,8 @@ _tutorial:
step1_1: "Ласкаво просимо!"
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані."
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших."
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис."
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас."
step2_1: "Перш ніж зробити запис або підписатись на когось, заповніть свій профіль."
step2_2: "Надання деякої інформації про себе допоможе іншим користувачам вирішити підписатись на вас."
step3_1: "Ви успішно налаштували свій обліковий запис?"
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."

View file

@ -326,7 +326,7 @@ connectService: "己連結"
disconnectService: "己斷開 "
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
registration: "註冊"
enableRegistration: "開啟新使用者註冊"
invite: "邀請"
@ -389,8 +389,8 @@ aboutMisskey: "關於 CherryPick"
administrator: "管理員"
token: "權杖"
twoStepAuthentication: "兩階段驗證"
moderator: "審員"
moderation: "監察"
moderator: "審員"
moderation: "審查"
nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰"
securityKeyName: "金鑰名稱"
@ -607,7 +607,7 @@ testEmail: "測試郵件發送"
wordMute: "被靜音的文字"
regexpError: "正規表達式錯誤"
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
instanceMute: "實例的靜音"
instanceMute: "被靜音的實例"
userSaysSomething: "{name}說了什麼"
makeActive: "啟用"
display: "檢視"
@ -1181,7 +1181,7 @@ _role:
name: "角色名稱"
description: "角色描述 "
permission: "角色的權限"
descriptionOfPermission: "<b>審核員</b>執行與審核相關的基本操作。\n<b>管理員</b>能變更實例的全部設定"
descriptionOfPermission: "<b>審查員</b>執行與審查相關的基本操作。\n<b>管理員</b>能變更實例的全部設定"
assignTarget: "指派目標"
descriptionOfAssignTarget: "<b>手動</b>是以手動管理這個角色包含的人員。\n<b>符合條件</b>是設定條件以自動包含符合條件的使用者。"
manual: "手動"
@ -1198,8 +1198,8 @@ _role:
iconUrl: "圖示的URL"
asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
canEditMembersByModerator: "允許編輯監察員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級"
_priority:
low: "低"
@ -1236,7 +1236,7 @@ _role:
or: "~或~"
not: "~否"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度"
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
setSensitiveFlagAutomatically: "設定 NSFW 旗標"

View file

@ -1,6 +1,6 @@
{
"name": "cherrypick",
"version": "13.4.0",
"version": "13.5.2",
"codename": "nasubi",
"repository": {
"type": "git",
@ -19,7 +19,7 @@
"start": "cd packages/backend && node ./built/boot/index.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
"init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm typeorm migration:run -d ormconfig.js",
"migrate": "cd packages/backend && pnpm migrate",
"migrateandstart": "pnpm migrate && pnpm start",
"gulp": "pnpm exec gulp build",
"watch": "pnpm dev",
@ -28,8 +28,8 @@
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"jest": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand",
"jest-and-coverage": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand",
"jest": "cd packages/backend && pnpm jest",
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage",
"format": "pnpm exec gulp format",

View file

@ -1,6 +1,6 @@
import { DataSource } from 'typeorm';
import { loadConfig } from './built/config.js';
import { entities } from './built/postgre.js';
import { entities } from './built/postgres.js';
const config = loadConfig();

View file

@ -4,7 +4,7 @@ import { DataSource } from 'typeorm';
import { createRedisConnection } from '@/redis.js';
import { DI } from './di-symbols.js';
import { loadConfig } from './config.js';
import { createPostgreDataSource } from './postgre.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
@ -18,7 +18,7 @@ const $config: Provider = {
const $db: Provider = {
provide: DI.db,
useFactory: async (config) => {
const db = createPostgreDataSource(config);
const db = createPostgresDataSource(config);
return await db.initialize();
},
inject: [DI.config],

View file

@ -48,6 +48,10 @@ export class ApImageService {
throw new Error('invalid image: url not privided');
}
if (!image.url.startsWith('https://')) {
throw new Error('invalid image: unexpected shcema of url: ' + image.url);
}
this.logger.info(`Creating the Image: ${image.url}`);
const instance = await this.metaService.fetch();

View file

@ -1,8 +1,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit';
import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js';
import type { UsersRepository } from '@/models/index.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { CacheableRemoteUser } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
@ -18,6 +17,7 @@ import { PollService } from '@/core/PollService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MessagingService } from '@/core/MessagingService.js';
import { bindThis } from '@/decorators.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js';
@ -32,7 +32,6 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ApNoteService {
@ -134,6 +133,16 @@ export class ApNoteService {
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
if (note.id && !note.id.startsWith('https://')) {
throw new Error('unexpected shcema of note.id: ' + note.id);
}
const url = getOneApHrefNullable(note.url);
if (url && !url.startsWith('https://')) {
throw new Error('unexpected shcema of note url: ' + url);
}
this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
@ -308,7 +317,7 @@ export class ApNoteService {
apEmojis,
poll,
uri: note.id,
url: getOneApHrefNullable(note.url),
url: url,
}, silent);
}

View file

@ -252,6 +252,12 @@ export class ApPersonService implements OnModuleInit {
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
if (url && !url.startsWith('https://')) {
throw new Error('unexpected shcema of person url: ' + url);
}
// Create user
let user: IRemoteUser;
try {
@ -283,7 +289,7 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url: getOneApHrefNullable(person.url),
url: url,
fields,
birthday: bday ? bday[0] : null,
location: person['vcard:Address'] ?? null,
@ -425,6 +431,12 @@ export class ApPersonService implements OnModuleInit {
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
if (url && !url.startsWith('https://')) {
throw new Error('unexpected shcema of person url: ' + url);
}
const updates = {
lastFetchedAt: new Date(),
inbox: person.inbox,
@ -459,7 +471,7 @@ export class ApPersonService implements OnModuleInit {
}
await this.userProfilesRepository.update({ userId: exist.id }, {
url: getOneApHrefNullable(person.url),
url: url,
fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
birthday: bday ? bday[0] : null,

View file

@ -20,6 +20,7 @@ type PackOptions = {
withUser?: boolean,
};
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
@Injectable()
export class DriveFileEntityService {
@ -82,8 +83,10 @@ export class DriveFileEntityService {
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
if (!(mode === 'static' && file.type.startsWith('video'))) {
return proxiedUrl(file.uri);
}
}
// リモートかつ期限切れはローカルプロキシを試みる
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
@ -91,20 +94,19 @@ export class DriveFileEntityService {
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
const url = `${this.config.url}/files/${key}`;
if (mode === 'avatar') return proxiedUrl(url);
if (mode === 'avatar') return proxiedUrl(file.uri);
return url;
}
}
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/avif', 'image/svg+xml'].includes(file.type);
if (mode === 'static') {
return file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null);
}
const url = file.webpublicUrl ?? file.url;
if (mode === 'avatar') return proxiedUrl(url);
if (mode === 'static') {
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null);
}
if (mode === 'avatar') {
return proxiedUrl(url);
}
return url;
}

View file

@ -197,7 +197,7 @@ export const entities = [
const log = process.env.NODE_ENV !== 'production';
export function createPostgreDataSource(config: Config) {
export function createPostgresDataSource(config: Config) {
return new DataSource({
type: 'postgres',
host: config.db.host,

View file

@ -11,7 +11,7 @@ import FormData from 'form-data';
import { DataSource } from 'typeorm';
import got, { RequestError } from 'got';
import loadConfig from '../src/config/load.js';
import { entities } from '../src/postgre.js';
import { entities } from '@/postgres.js';
import type * as misskey from 'misskey-js';
const _filename = fileURLToPath(import.meta.url);

View file

@ -1,6 +1,6 @@
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
<div ref="emojisEl" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
@ -327,6 +327,11 @@ function paste(event: ClipboardEvent): void {
}
}
function onEnter(ev: KeyboardEvent) {
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
done();
}
function done(query?: string): boolean | void {
if (query == null) query = q.value;
if (query == null || typeof query !== 'string') return;

View file

@ -6,15 +6,14 @@
<span>{{ $ts.clickToShow }}</span>
</div>
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
<audio
ref="audioEl"
class="audio"
<VuePlyr :options="{ volume: 0.5 }">
<audio controls preload="metadata">
<source
:src="media.url"
:title="media.name"
controls
preload="metadata"
@volumechange="volumechange"
:type="media.type"
/>
</audio>
</VuePlyr>
</div>
<a
v-else class="download"
@ -31,7 +30,9 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import * as misskey from 'misskey-js';
import VuePlyr from 'vue-plyr';
import { ColdDeviceStorage } from '@/store';
import 'vue-plyr/dist/vue-plyr.css';
const props = withDefaults(defineProps<{
media: misskey.entities.DriveFile;
@ -55,7 +56,11 @@ onMounted(() => {
width: 100%;
border-radius: 4px;
margin-top: 4px;
overflow: hidden;
overflow: clip;
--plyr-color-main: var(--accent);
--plyr-audio-controls-background: var(--bg);
--plyr-audio-controls-color: var(--accentLighten);
> .download,
> .sensitive {
@ -93,10 +98,8 @@ onMounted(() => {
}
> .audio {
.audio {
display: block;
width: 100%;
}
border-radius: 8px;
overflow: clip;
}
}
</style>

View file

@ -1,11 +1,11 @@
<template>
<div class="hoawjimk">
<div>
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length">
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
<div ref="gallery" :class="[$style.medias, count <= 4 ? $style['n' + count] : $style.nMany]">
<template v-for="media in mediaList.filter(media => previewable(media))">
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :class="$style.media" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" :class="$style.media" class="image" :data-id="media.id" :image="media" :raw="raw"/>
</template>
</div>
</div>
@ -32,6 +32,7 @@ const props = defineProps<{
const gallery = ref(null);
const pswpZIndex = os.claimZIndex('middle');
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
@ -122,82 +123,61 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
};
</script>
<style lang="scss" scoped>
.hoawjimk {
> .gird-container {
<style lang="scss" module>
.container {
position: relative;
width: 100%;
margin-top: 4px;
}
&:before {
content: '';
display: block;
padding-top: 56.25% // 16:9;
}
> div {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
.medias {
display: grid;
grid-gap: 8px;
> * {
overflow: hidden;
border-radius: 6px;
}
&[data-count="1"] {
&.n1 {
aspect-ratio: 16/9;
grid-template-rows: 1fr;
}
&[data-count="2"] {
&.n2 {
aspect-ratio: 16/9;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
}
&[data-count="3"] {
&.n3 {
aspect-ratio: 16/9;
grid-template-columns: 1fr 0.5fr;
grid-template-rows: 1fr 1fr;
> *:nth-child(1) {
> .media:nth-child(1) {
grid-row: 1 / 3;
}
> *:nth-child(3) {
> .media:nth-child(3) {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
}
&[data-count="4"] {
&.n4 {
aspect-ratio: 16/9;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
> *:nth-child(1) {
grid-column: 1 / 2;
grid-row: 1 / 2;
}
&.nMany {
grid-template-columns: 1fr 1fr;
> *:nth-child(2) {
grid-column: 2 / 3;
grid-row: 1 / 2;
> .media {
aspect-ratio: 16/9;
}
}
}
> *:nth-child(3) {
grid-column: 1 / 2;
grid-row: 2 / 3;
}
> *:nth-child(4) {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
}
}
.media {
overflow: hidden; // clip
border-radius: 8px;
}
</style>

View file

@ -6,7 +6,7 @@
</div>
</div>
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
<vue-plyr>
<VuePlyr :options="{ volume: 0.5 }">
<video
controls
:data-poster="video.thumbnailUrl"
@ -17,7 +17,7 @@
:type="video.type"
/>
</video>
</vue-plyr>
</VuePlyr>
<i class="ti ti-eye-off" @click="hide = true"></i>
</div>
</template>

View file

@ -48,7 +48,7 @@
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else :class="$style.translated">

View file

@ -4,7 +4,7 @@
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<details v-if="note.files.length > 0">

View file

@ -195,7 +195,7 @@ export default defineComponent({
return h(MkSparkle, {}, genEl(token.children));
}
case 'rotate': {
const degrees = parseFloat(token.props.args.deg) ?? '90';
const degrees = parseFloat(token.props.args.deg ?? '90');
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
break;
}

View file

@ -91,7 +91,7 @@ function regenerateToken() {
type: 'password',
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/regenerate_token', {
os.api('i/regenerate-token', {
password: password,
});
});

View file

@ -169,7 +169,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
disableShowingAnimatedImages: {
where: 'device',
default: false,
default: matchMedia('(prefers-reduced-motion)').matches,
},
emojiStyle: {
where: 'device',

View file

@ -166,6 +166,13 @@ hr {
-webkit-touch-callout: none;
}
._nowrap {
white-space: pre !important;
word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html
overflow: hidden;
text-overflow: ellipsis;
}
._ghost {
@extend ._noSelect;
pointer-events: none;