Compare commits
17 Commits
cdcaed1203
...
4c97540025
Author | SHA1 | Date |
---|---|---|
|
4c97540025 | |
|
d522d1bf26 | |
|
080276e3e7 | |
|
619bb2214e | |
|
c23f2ff900 | |
|
14d6734cb1 | |
|
3bdb1dd558 | |
|
e75d749784 | |
|
48232ca57b | |
|
3564bf5c66 | |
|
685fc2bd9d | |
|
6cc0138d1e | |
|
d3228d5570 | |
|
4a8ffe20a7 | |
|
5615675991 | |
|
0c65b8058a | |
|
82bec76cd4 |
|
@ -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"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
22.11.0
|
22.15.0
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -781,7 +787,7 @@
|
||||||
- Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
|
- Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
- Fix: FTT有効かつDBフォールバック有効時、STLのようにタイムラインのソースが複数だとFTTとDBのフォールバック間で取得されないノートがある問題
|
||||||
|
|
||||||
## 2024.3.0
|
## 2024.3.0
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -5413,6 +5413,10 @@ export interface Locale extends ILocale {
|
||||||
* フォルダを作って整理することもできます。
|
* フォルダを作って整理することもできます。
|
||||||
*/
|
*/
|
||||||
"driveAboutTip": string;
|
"driveAboutTip": string;
|
||||||
|
/**
|
||||||
|
* スクロールして閉じる
|
||||||
|
*/
|
||||||
|
"scrollToClose": string;
|
||||||
"_chat": {
|
"_chat": {
|
||||||
/**
|
/**
|
||||||
* まだメッセージはありません
|
* まだメッセージはありません
|
||||||
|
|
|
@ -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: "まだメッセージはありません"
|
||||||
|
|
|
@ -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 發送訊息"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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' });
|
|
@ -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",
|
||||||
|
|
|
@ -38,6 +38,7 @@ type TimelineOptions = {
|
||||||
excludePureRenotes: boolean;
|
excludePureRenotes: boolean;
|
||||||
ignoreAuthorFromUserSuspension?: boolean;
|
ignoreAuthorFromUserSuspension?: boolean;
|
||||||
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
|
dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
|
||||||
|
preventEmptyTimelineDbFallback?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -71,12 +72,20 @@ export class FanoutTimelineEndpointService {
|
||||||
|
|
||||||
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
||||||
|
|
||||||
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
|
// オプション無効時、取得したredisResultのうち、2つ以上ソースがあり、1つでも空であればDBにフォールバックする
|
||||||
const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
|
let shouldFallbackToDb = ps.useDbFallback &&
|
||||||
|
(ps.preventEmptyTimelineDbFallback !== true && redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
|
||||||
|
|
||||||
|
// 取得したresultの中で最古のIDのうち、最も新しいものを取得
|
||||||
|
const fttThresholdId = redisResult.map(ids => ids[0]).sort()[0];
|
||||||
|
|
||||||
|
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
|
||||||
|
const redisResultIds = shouldFallbackToDb ? [] : Array.from(new Set(redisResult.flat(1))).sort(idCompare);
|
||||||
|
|
||||||
|
let noteIds = redisResultIds.filter(id => id >= fttThresholdId).slice(0, ps.limit);
|
||||||
|
|
||||||
let noteIds = redisResultIds.slice(0, ps.limit);
|
|
||||||
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
|
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
|
||||||
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
|
shouldFallbackToDb ||= ps.useDbFallback && (noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId);
|
||||||
|
|
||||||
if (!shouldFallbackToDb) {
|
if (!shouldFallbackToDb) {
|
||||||
let filter = ps.noteFilter ?? (_note => true);
|
let filter = ps.noteFilter ?? (_note => true);
|
||||||
|
|
|
@ -150,6 +150,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
withFiles: ps.withFiles,
|
withFiles: ps.withFiles,
|
||||||
withRenotes: ps.withRenotes,
|
withRenotes: ps.withRenotes,
|
||||||
}, me),
|
}, me),
|
||||||
|
preventEmptyTimelineDbFallback: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return timeline;
|
return timeline;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,27 +88,39 @@ 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を使うとなんか挙動がおかしいので、TouchEventとMouseEventを使い分ける
|
window.addEventListener('mousemove', moving, { passive: true });
|
||||||
if (event.pointerType === 'mouse') {
|
window.addEventListener('mouseup', () => {
|
||||||
window.addEventListener('mousemove', moving, { passive: true });
|
window.removeEventListener('mousemove', moving);
|
||||||
window.addEventListener('mouseup', () => {
|
onPullRelease();
|
||||||
window.removeEventListener('mousemove', moving);
|
}, { passive: true, once: true });
|
||||||
onPullRelease();
|
}
|
||||||
}, { passive: true, once: true });
|
|
||||||
} else {
|
function moveStartByTouch(event: TouchEvent) {
|
||||||
window.addEventListener('touchmove', moving, { passive: true });
|
if (isRefreshing.value) return;
|
||||||
window.addEventListener('touchend', () => {
|
|
||||||
window.removeEventListener('touchmove', moving);
|
const scrollPos = scrollEl!.scrollTop;
|
||||||
onPullRelease();
|
if (scrollPos !== 0) {
|
||||||
}, { passive: true, once: true });
|
unlockDownScroll();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lockDownScroll();
|
||||||
|
|
||||||
|
isPulling.value = true;
|
||||||
|
startScreenY = getScreenY(event);
|
||||||
|
pullDistance.value = 0;
|
||||||
|
|
||||||
|
window.addEventListener('touchmove', moving, { passive: true });
|
||||||
|
window.addEventListener('touchend', () => {
|
||||||
|
window.removeEventListener('touchmove', moving);
|
||||||
|
onPullRelease();
|
||||||
|
}, { passive: true, once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveBySystem(to: number): Promise<void> {
|
function moveBySystem(to: number): Promise<void> {
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -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 }">
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue