Compare commits

...

10 Commits

Author SHA1 Message Date
CGsama cc9f245fc5
Merge 0636de6a52 into d522d1bf26 2025-05-04 12:57:46 +00:00
Sayamame-beans d522d1bf26
docs(changelog): add information for #15924 (#15947) 2025-05-04 20:59:24 +09:00
github-actions[bot] 080276e3e7 Bump version to 2025.5.0-alpha.1 2025-05-04 10:07:59 +00:00
syuilo 619bb2214e
New Crowdin updates (#15935)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (English)
2025-05-04 19:00:56 +09:00
renovate[bot] c23f2ff900
chore(deps): update node.js to v22.15.0 (#15606)
* chore(deps): update node.js to v22.15.0

* chore: determine Jest args from Node.js version

* fix

* fix: `import.meta.dirname` is not supported in v20.10.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-05-04 19:00:36 +09:00
syuilo 14d6734cb1
Fix MkPullToRefresh behaviour (#15944)
* Update MkPullToRefresh.vue

* Update MkPullToRefresh.vue

* Update MkPullToRefresh.vue
2025-05-04 18:51:30 +09:00
syuilo 3bdb1dd558 🎨 2025-05-04 17:32:09 +09:00
かっこかり e75d749784
fix(frontend): ダイアログのお知らせが画面からはみ出ることがある問題を修正 (#15878)
* fix(frontend): ダイアログのお知らせが画面からはみ出ることがある問題を修正

* Update Changelog

* 🎨

* 🎨

* enhance: スクロールしないと閉じられないように

* Update CHANGELOG.md
2025-05-04 15:50:05 +09:00
CGsama 0636de6a52 Fix 2025-01-26 18:10:45 -05:00
CGsama fb8312f989 Embedded all attachment, renotes and discussion history into rss feed content & improve title 2025-01-26 15:39:24 -05:00
19 changed files with 261 additions and 50 deletions

View File

@ -5,7 +5,7 @@
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "22.11.0" "version": "22.15.0"
}, },
"ghcr.io/devcontainers-extra/features/pnpm:2": { "ghcr.io/devcontainers-extra/features/pnpm:2": {
"version": "10.10.0" "version": "10.10.0"

View File

@ -1 +1 @@
22.11.0 22.15.0

View File

@ -1,5 +1,8 @@
## 2025.5.0 ## 2025.5.0
### Note
- DockerのNode.jsが22.15.0に更新されました
### General ### General
- -
@ -8,6 +11,7 @@
- アクセシビリティ設定からオフにすることもできます - アクセシビリティ設定からオフにすることもできます
- Enhance: タイムラインのパフォーマンスを向上 - Enhance: タイムラインのパフォーマンスを向上
- Fix: 一部のブラウザでアコーディオンメニューのアニメーションが動作しない問題を修正 - Fix: 一部のブラウザでアコーディオンメニューのアニメーションが動作しない問題を修正
- Fix: ダイアログのお知らせが画面からはみ出ることがある問題を修正
### Server ### Server
- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775` - Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775`
@ -20,6 +24,8 @@
- Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175) - Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175)
- Fix: ファイルをアップロードした際にファイル名が常に untitled になる問題を修正 - Fix: ファイルをアップロードした際にファイル名が常に untitled になる問題を修正
- Fix: ファイルのアップロードに失敗することがある問題を修正 - Fix: ファイルのアップロードに失敗することがある問題を修正
- 投稿フォーム上で画像のクロップを行うと、`Invalid Param.`エラーでノートが投稿出来なくなる問題も解決されます。
- この事象によって既にノートが投稿出来ない状態になっている場合は、投稿フォーム右上のメニューから、下書きデータの「リセット」を行ってください。
## 2025.4.1 ## 2025.4.1

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4 # syntax = docker/dockerfile:1.4
ARG NODE_VERSION=22.11.0-bookworm ARG NODE_VERSION=22.15.0-bookworm
# build assets & compile TypeScript # build assets & compile TypeScript

View File

@ -1425,6 +1425,7 @@ _settings:
ifOff: "Quan es desactiva" ifOff: "Quan es desactiva"
enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius" enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius"
enablePullToRefresh: "Lliscar i actualitzar " enablePullToRefresh: "Lliscar i actualitzar "
enablePullToRefresh_description: "Amb el ratolí, llisca mentre prems la roda."
_chat: _chat:
showSenderName: "Mostrar el nom del remitent" showSenderName: "Mostrar el nom del remitent"
sendOnEnter: "Introdueix per enviar" sendOnEnter: "Introdueix per enviar"

View File

@ -1348,6 +1348,7 @@ readonly: "Read only"
goToDeck: "Return to Deck" goToDeck: "Return to Deck"
federationJobs: "Federation Jobs" federationJobs: "Federation Jobs"
driveAboutTip: "In Drive, a list of files you've uploaded in the past will be displayed. <br> \nYou can reuse these files when attaching them to notes, or you can upload files in advance to post later. <br> \n<b>Be careful when deleting a file, as it will not be available in all places where it was used (such as notes, pages, avatars, banners, etc.).</b> <br> \nYou can also create folders to organize your files." driveAboutTip: "In Drive, a list of files you've uploaded in the past will be displayed. <br> \nYou can reuse these files when attaching them to notes, or you can upload files in advance to post later. <br> \n<b>Be careful when deleting a file, as it will not be available in all places where it was used (such as notes, pages, avatars, banners, etc.).</b> <br> \nYou can also create folders to organize your files."
scrollToClose: "Scroll to close"
_chat: _chat:
noMessagesYet: "No messages yet" noMessagesYet: "No messages yet"
newMessage: "New message" newMessage: "New message"
@ -1425,6 +1426,7 @@ _settings:
ifOff: "When turned off" ifOff: "When turned off"
enableSyncThemesBetweenDevices: "Synchronize installed themes across devices" enableSyncThemesBetweenDevices: "Synchronize installed themes across devices"
enablePullToRefresh: "Pull to Refresh" enablePullToRefresh: "Pull to Refresh"
enablePullToRefresh_description: "When using a mouse, drag while pressing in the scrolling wheel."
_chat: _chat:
showSenderName: "Show sender's name" showSenderName: "Show sender's name"
sendOnEnter: "Press Enter to send" sendOnEnter: "Press Enter to send"

4
locales/index.d.ts vendored
View File

@ -5413,6 +5413,10 @@ export interface Locale extends ILocale {
* *
*/ */
"driveAboutTip": string; "driveAboutTip": string;
/**
*
*/
"scrollToClose": string;
"_chat": { "_chat": {
/** /**
* *

View File

@ -1348,6 +1348,7 @@ readonly: "読み取り専用"
goToDeck: "デッキへ戻る" goToDeck: "デッキへ戻る"
federationJobs: "連合ジョブ" federationJobs: "連合ジョブ"
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。" driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。"
scrollToClose: "スクロールして閉じる"
_chat: _chat:
noMessagesYet: "まだメッセージはありません" noMessagesYet: "まだメッセージはありません"

View File

@ -1348,6 +1348,7 @@ readonly: "唯讀"
goToDeck: "回去甲板" goToDeck: "回去甲板"
federationJobs: "聯邦通訊作業" federationJobs: "聯邦通訊作業"
driveAboutTip: "在「雲端硬碟」中,會顯示過去上傳的檔案列表。<br>\n可以在附加到貼文時重新利用或者事先上傳之後再用於發布。<br>\n<b>請注意,刪除檔案後,之前使用過該檔案的所有地方(貼文、頁面、大頭貼、橫幅等)也會一併無法顯示。</b><br>\n也可以建立資料夾來整理檔案。" driveAboutTip: "在「雲端硬碟」中,會顯示過去上傳的檔案列表。<br>\n可以在附加到貼文時重新利用或者事先上傳之後再用於發布。<br>\n<b>請注意,刪除檔案後,之前使用過該檔案的所有地方(貼文、頁面、大頭貼、橫幅等)也會一併無法顯示。</b><br>\n也可以建立資料夾來整理檔案。"
scrollToClose: "用滾輪關閉"
_chat: _chat:
noMessagesYet: "尚無訊息" noMessagesYet: "尚無訊息"
newMessage: "新訊息" newMessage: "新訊息"
@ -1425,6 +1426,7 @@ _settings:
ifOff: "關閉時" ifOff: "關閉時"
enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題" enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題"
enablePullToRefresh: "下拉更新" enablePullToRefresh: "下拉更新"
enablePullToRefresh_description: "使用滑鼠,按下並拖曳滾輪。"
_chat: _chat:
showSenderName: "顯示發送者的名稱" showSenderName: "顯示發送者的名稱"
sendOnEnter: "按下 Enter 發送訊息" sendOnEnter: "按下 Enter 發送訊息"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.5.0-alpha.0", "version": "2025.5.0-alpha.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,4 +1,5 @@
import tsParser from '@typescript-eslint/parser'; import tsParser from '@typescript-eslint/parser';
import globals from 'globals';
import sharedConfig from '../shared/eslint.config.js'; import sharedConfig from '../shared/eslint.config.js';
export default [ export default [
@ -6,6 +7,13 @@ export default [
{ {
ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'], ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
}, },
{
languageOptions: {
globals: {
...globals.node,
},
},
},
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
languageOptions: { languageOptions: {

20
packages/backend/jest.js Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
import child_process from 'node:child_process';
import path from 'node:path';
import url from 'node:url';
import semver from 'semver';
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = [];
args.push(...[
...semver.satisfies(process.version, '^20.17.0 || ^22.0.0') ? ['--no-experimental-require-module'] : [],
'--experimental-vm-modules',
'--experimental-import-meta-resolve',
path.join(__dirname, 'node_modules/jest/bin/jest.js'),
...process.argv.slice(2),
]);
child_process.spawn(process.execPath, args, { stdio: 'inherit' });

View File

@ -22,12 +22,12 @@
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit", "typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"", "eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint", "lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs", "jest": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs", "jest:e2e": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs",
"jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs", "jest:fed": "node ./jest.js --forceExit --config jest.config.fed.cjs",
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs", "jest-and-coverage": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs",
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs", "jest-and-coverage:e2e": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "jest-clear": "cross-env NODE_ENV=test node ./jest.js --clearCache",
"test": "pnpm jest", "test": "pnpm jest",
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
"test:fed": "pnpm jest:fed", "test:fed": "pnpm jest:fed",

View File

@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { In, IsNull } from 'typeorm'; import { In, IsNull } from 'typeorm';
import { Feed } from 'feed'; import { Feed } from 'feed';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, NotesRepository, UserProfilesRepository } from '@/models/_.js'; import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -16,13 +16,23 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { MfmService } from "@/core/MfmService.js"; import { MfmService } from "@/core/MfmService.js";
import { parse as mfmParse } from 'mfm-js'; import { parse as mfmParse } from 'mfm-js';
import { MiNote } from '@/models/Note.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
@Injectable() @Injectable()
export class FeedService { export class FeedService {
private readonly logger: Logger;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@ -36,13 +46,17 @@ export class FeedService {
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private idService: IdService, private idService: IdService,
private mfmService: MfmService, private mfmService: MfmService,
loggerService: LoggerService,
) { ) {
this.logger = loggerService.getLogger('feed');
} }
@bindThis @bindThis
public async packFeed(user: MiUser) { public async packFeed(user: MiUser) {
const author = { const author = {
link: `${this.config.url}/@${user.username}`, link: `${this.config.url}/@${user.username}`,
email: `${user.username}@${this.config.host}`,
name: user.name ?? user.username, name: user.name ?? user.username,
}; };
@ -51,7 +65,6 @@ export class FeedService {
const notes = await this.notesRepository.find({ const notes = await this.notesRepository.find({
where: { where: {
userId: user.id, userId: user.id,
renoteId: IsNull(),
visibility: In(['public', 'home']), visibility: In(['public', 'home']),
}, },
order: { id: -1 }, order: { id: -1 },
@ -75,22 +88,111 @@ export class FeedService {
}); });
for (const note of notes) { for (const note of notes) {
const files = note.fileIds.length > 0 ? await this.driveFilesRepository.findBy({ let contentStr = await this.noteToString(note, true);
id: In(note.fileIds), let next = note.renoteId ? note.renoteId : note.replyId;
}) : []; let depth = 10;
const file = files.find(file => file.type.startsWith('image/')); const noteintitle = true;
const text = note.text; let title = `Post by ${author.name}`;
while (depth > 0 && next) {
const finding = await this.findById(next);
contentStr += finding.text;
next = finding.next;
depth -= 1;
}
if (noteintitle) {
if (note.renoteId) {
title = `Boost by ${author.name}`;
} else if (note.replyId) {
title = `Reply by ${author.name}`;
} else {
title = `Post by ${author.name}`;
}
const effectiveNote =
!isQuote(note) && note.renote != null ? note.renote : note;
const content = getNoteSummary(effectiveNote);
if (content) {
title += `: ${content}`;
}
}
feed.addItem({ feed.addItem({
title: `New note by ${author.name}`, title: this.escapeCDATA(title).substring(0, 100),
link: `${this.config.url}/notes/${note.id}`, link: `${this.config.url}/notes/${note.id}`,
date: this.idService.parse(note.id).date, date: this.idService.parse(note.id).date,
description: note.cw ?? undefined, description: this.escapeCDATA(note.cw) ?? undefined,
content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined, content: this.escapeCDATA(contentStr) || undefined,
image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
}); });
} }
return feed; return feed;
} }
private escapeCDATA(str: string) {
return str?.replaceAll("]]>", "]]]]><![CDATA[>").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
}
private async noteToString(note: MiNote, isTheNote = false) {
const author = isTheNote
? null
: await this.usersRepository.findOneByOrFail({ id: note.userId });
let outstr = author
? `${author.name}(@${author.username}@${
author.host ? author.host : this.config.host
}) ${
note.renoteId ? "renotes" : note.replyId ? "replies" : "says"
}: <br>`
: "";
const files = note.fileIds?.length ? await this.driveFilesRepository.findBy({
id: In(note.fileIds),
}) : [];
let fileEle = "";
for (const file of files) {
if (file.type.startsWith("image/")) {
fileEle += ` <br><img src="${this.driveFileEntityService.getPublicUrl(file)}">`;
} else if (file.type.startsWith("audio/")) {
fileEle += ` <br><audio controls src="${this.driveFileEntityService.getPublicUrl(
file,
)}" type="${file.type}">`;
} else if (file.type.startsWith("video/")) {
fileEle += ` <br><video controls src="${this.driveFileEntityService.getPublicUrl(
file,
)}" type="${file.type}">`;
} else {
fileEle += ` <br><a href="${this.driveFileEntityService.getPublicUrl(file)}" download="${
file.name
}">${file.name}</a>`;
}
}
outstr += `${note.cw ? note.cw + "<br>" : ""}${
(note.text ? this.mfmService.toHtml(mfmParse(note.text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined) || ""
}${fileEle}`;
if (isTheNote) {
outstr += ` <span class="${
note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"
} ${
fileEle.indexOf("img src") !== -1 ? "with_img" : "without_img"
}"></span>`;
}
return outstr;
}
private async findById(id : string) {
let text = "";
let next = null;
const findings = await this.notesRepository.findOneBy({
id: id,
visibility: In(['public', 'home']),
});
if (findings) {
text += `<hr>`;
text += await this.noteToString(findings);
next = findings.renoteId ? findings.renoteId : findings.replyId;
} else {
this.logger.info(`Note ${id} not in scope`);
}
return { text, next };
}
} }

View File

@ -50,6 +50,10 @@ services:
source: ../jest.config.fed.cjs source: ../jest.config.fed.cjs
target: /misskey/packages/backend/jest.config.fed.cjs target: /misskey/packages/backend/jest.config.fed.cjs
read_only: true read_only: true
- type: bind
source: ../jest.js
target: /misskey/packages/backend/jest.js
read_only: true
- type: bind - type: bind
source: ../../misskey-js/built source: ../../misskey-js/built
target: /misskey/packages/misskey-js/built target: /misskey/packages/misskey-js/built

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkModal ref="modal" :zPriority="'middle'" @closed="$emit('closed')" @click="onBgClick"> <MkModal ref="modal" :zPriority="'middle'" :preferType="'dialog'" @closed="$emit('closed')" @click="onBgClick">
<div ref="rootEl" :class="$style.root"> <div ref="rootEl" :class="$style.root">
<div :class="$style.header"> <div :class="$style.header">
<span :class="$style.icon"> <span :class="$style.icon">
@ -16,13 +16,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.title">{{ announcement.title }}</span> <span :class="$style.title">{{ announcement.title }}</span>
</div> </div>
<div :class="$style.text"><Mfm :text="announcement.text"/></div> <div :class="$style.text"><Mfm :text="announcement.text"/></div>
<MkButton primary full @click="ok">{{ i18n.ts.ok }}</MkButton> <div ref="bottomEl"></div>
<div :class="$style.footer">
<MkButton
primary
full
:disabled="!hasReachedBottom"
@click="ok"
>{{ hasReachedBottom ? i18n.ts.close : i18n.ts.scrollToClose }}</MkButton>
</div>
</div> </div>
</MkModal> </MkModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, useTemplateRef } from 'vue'; import { onMounted, ref, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
@ -32,12 +40,12 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
import { updateCurrentAccountPartial } from '@/accounts.js'; import { updateCurrentAccountPartial } from '@/accounts.js';
const props = withDefaults(defineProps<{ const props = defineProps<{
announcement: Misskey.entities.Announcement; announcement: Misskey.entities.Announcement;
}>(), { }>();
});
const rootEl = useTemplateRef('rootEl'); const rootEl = useTemplateRef('rootEl');
const bottomEl = useTemplateRef('bottomEl');
const modal = useTemplateRef('modal'); const modal = useTemplateRef('modal');
async function ok() { async function ok() {
@ -72,7 +80,34 @@ function onBgClick() {
}); });
} }
const hasReachedBottom = ref(false);
onMounted(() => { onMounted(() => {
if (bottomEl.value && rootEl.value) {
const bottomElRect = bottomEl.value.getBoundingClientRect();
const rootElRect = rootEl.value.getBoundingClientRect();
if (
bottomElRect.top >= rootElRect.top &&
bottomElRect.top <= (rootElRect.bottom - 66) // 66 75 * 0.9 (modal)
) {
hasReachedBottom.value = true;
return;
}
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
hasReachedBottom.value = true;
observer.disconnect();
}
}
}, {
root: rootEl.value,
rootMargin: '0px 0px -75px 0px',
});
observer.observe(bottomEl.value);
}
}); });
</script> </script>
@ -80,9 +115,12 @@ onMounted(() => {
.root { .root {
margin: auto; margin: auto;
position: relative; position: relative;
padding: 32px; padding: 32px 32px 0;
min-width: 320px; min-width: 320px;
max-width: 480px; max-width: 480px;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box; box-sizing: border-box;
background: var(--MI_THEME-panel); background: var(--MI_THEME-panel);
border-radius: var(--MI-radius); border-radius: var(--MI-radius);
@ -103,4 +141,14 @@ onMounted(() => {
.text { .text {
margin: 1em 0; margin: 1em 0;
} }
.footer {
position: sticky;
bottom: 0;
left: -32px;
backdrop-filter: var(--MI-blur, blur(15px));
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
margin: 0 -32px;
padding: 24px 32px;
}
</style> </style>

View File

@ -76,8 +76,8 @@ function unlockDownScroll() {
scrollEl.style.overscrollBehavior = 'contain'; scrollEl.style.overscrollBehavior = 'contain';
} }
function moveStart(event: PointerEvent) { function moveStartByMouse(event: MouseEvent) {
if (event.pointerType === 'mouse' && event.button !== 1) return; if (event.button !== 1) return;
if (isRefreshing.value) return; if (isRefreshing.value) return;
const scrollPos = scrollEl!.scrollTop; const scrollPos = scrollEl!.scrollTop;
@ -88,28 +88,40 @@ function moveStart(event: PointerEvent) {
lockDownScroll(); lockDownScroll();
// pull event.preventDefault(); //
window.document.body.setAttribute('inert', 'true');
isPulling.value = true; isPulling.value = true;
startScreenY = getScreenY(event); startScreenY = getScreenY(event);
pullDistance.value = 0; pullDistance.value = 0;
// PointerEvent使TouchEventMouseEvent使
if (event.pointerType === 'mouse') {
window.addEventListener('mousemove', moving, { passive: true }); window.addEventListener('mousemove', moving, { passive: true });
window.addEventListener('mouseup', () => { window.addEventListener('mouseup', () => {
window.removeEventListener('mousemove', moving); window.removeEventListener('mousemove', moving);
onPullRelease(); onPullRelease();
}, { passive: true, once: true }); }, { passive: true, once: true });
} else { }
function moveStartByTouch(event: TouchEvent) {
if (isRefreshing.value) return;
const scrollPos = scrollEl!.scrollTop;
if (scrollPos !== 0) {
unlockDownScroll();
return;
}
lockDownScroll();
isPulling.value = true;
startScreenY = getScreenY(event);
pullDistance.value = 0;
window.addEventListener('touchmove', moving, { passive: true }); window.addEventListener('touchmove', moving, { passive: true });
window.addEventListener('touchend', () => { window.addEventListener('touchend', () => {
window.removeEventListener('touchmove', moving); window.removeEventListener('touchmove', moving);
onPullRelease(); onPullRelease();
}, { passive: true, once: true }); }, { passive: true, once: true });
} }
}
function moveBySystem(to: number): Promise<void> { function moveBySystem(to: number): Promise<void> {
return new Promise(r => { return new Promise(r => {
@ -148,7 +160,6 @@ async function closeContent() {
} }
function onPullRelease() { function onPullRelease() {
window.document.body.removeAttribute('inert');
startScreenY = null; startScreenY = null;
if (isPulledEnough.value) { if (isPulledEnough.value) {
isPulledEnough.value = false; isPulledEnough.value = false;
@ -208,13 +219,15 @@ onMounted(() => {
if (rootEl.value == null) return; if (rootEl.value == null) return;
scrollEl = getScrollContainer(rootEl.value); scrollEl = getScrollContainer(rootEl.value);
lockDownScroll(); lockDownScroll();
rootEl.value.addEventListener('pointerdown', moveStart, { passive: true }); rootEl.value.addEventListener('mousedown', moveStartByMouse, { passive: false }); // preventDefault
rootEl.value.addEventListener('touchstart', moveStartByTouch, { passive: true });
rootEl.value.addEventListener('touchend', toggleScrollLockOnTouchEnd, { passive: true }); rootEl.value.addEventListener('touchend', toggleScrollLockOnTouchEnd, { passive: true });
}); });
onUnmounted(() => { onUnmounted(() => {
unlockDownScroll(); unlockDownScroll();
if (rootEl.value) rootEl.value.removeEventListener('pointerdown', moveStart); if (rootEl.value) rootEl.value.removeEventListener('mousedown', moveStartByMouse);
if (rootEl.value) rootEl.value.removeEventListener('touchstart', moveStartByTouch);
if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd); if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
}); });
</script> </script>

View File

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTl :events="timeline"> <MkTl :events="timeline">
<template #left="{ event }"> <template #left="{ event }">
<div> <div>
<MkAvatar :user="event.user" style="width: 24px; height: 24px;"/> <MkAvatar :user="event.user" style="width: 26px; height: 26px;"/>
</div> </div>
</template> </template>
<template #right="{ event, timestamp, delta }"> <template #right="{ event, timestamp, delta }">

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2025.5.0-alpha.0", "version": "2025.5.0-alpha.1",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",