summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-10-31 20:21:50 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-10-31 20:21:50 +0900
commit54631026de9edb28c842f612a37511f82f1f749e (patch)
tree885c7f28b9756899faf3a99842d008be8953903e
parentMerge branch 'develop' (diff)
parent12.95.0 (diff)
downloadmisskey-54631026de9edb28c842f612a37511f82f1f749e.tar.gz
misskey-54631026de9edb28c842f612a37511f82f1f749e.tar.bz2
misskey-54631026de9edb28c842f612a37511f82f1f749e.zip
Merge branch 'develop'
-rw-r--r--.circleci/config.yml49
-rw-r--r--.circleci/misskey/default.yml12
-rw-r--r--.github/CODEOWNERS38
-rw-r--r--.github/misskey/test.yml (renamed from .circleci/misskey/test.yml)0
-rw-r--r--.github/workflows/lint.yml21
-rw-r--r--.github/workflows/test.yml (renamed from .github/workflows/nodejs.yml)46
-rw-r--r--CHANGELOG.md12
-rw-r--r--README.md1
-rw-r--r--cypress.json2
-rw-r--r--cypress/integration/basic.js3
-rw-r--r--locales/ar-SA.yml275
-rw-r--r--locales/de-DE.yml2
-rw-r--r--locales/en-US.yml2
-rw-r--r--locales/eo-UY.yml1
-rw-r--r--locales/ja-JP.yml2
-rw-r--r--migration/1635500777168-note-thread-mute.ts26
-rw-r--r--package.json7
-rw-r--r--src/client/components/forgot-password.vue25
-rw-r--r--src/client/components/form/select.vue34
-rw-r--r--src/client/components/google.vue12
-rw-r--r--src/client/components/media-video.vue1
-rw-r--r--src/client/components/mention.vue65
-rw-r--r--src/client/components/note-detailed.vue15
-rw-r--r--src/client/components/note.vue15
-rw-r--r--src/client/init.ts21
-rw-r--r--src/client/pages/admin/emojis.vue12
-rw-r--r--src/client/pages/admin/index.vue10
-rw-r--r--src/client/scripts/login-id.ts11
-rw-r--r--src/client/themes/l-light.json51
-rw-r--r--src/client/ui/_common_/sidebar.vue2
-rw-r--r--src/db/postgre.ts2
-rw-r--r--src/docs/ar-SA/advanced/share-page.md2
-rw-r--r--src/docs/ar-SA/advanced/stream.md2
-rw-r--r--src/docs/ar-SA/features/note.md4
-rw-r--r--src/docs/ar-SA/general/glossary.md2
-rw-r--r--src/docs/ar-SA/general/links.md2
-rw-r--r--src/docs/en-US/features/keyboard-shortcut.md26
-rw-r--r--src/docs/en-US/features/pages.md4
-rw-r--r--src/docs/en-US/features/theme.md14
-rw-r--r--src/docs/en-US/features/timeline.md40
-rw-r--r--src/docs/eo-UY/advanced/share-page.md22
-rw-r--r--src/docs/pt-PT/features/custom-emoji.md2
-rw-r--r--src/docs/pt-PT/features/theme.md8
-rw-r--r--src/docs/pt-PT/features/widgets.md2
-rw-r--r--src/models/entities/note-thread-muting.ts33
-rw-r--r--src/models/entities/note.ts6
-rw-r--r--src/models/index.ts2
-rw-r--r--src/models/repositories/user.ts2
-rw-r--r--src/remote/activitypub/models/note.ts4
-rw-r--r--src/remote/activitypub/models/person.ts5
-rw-r--r--src/remote/activitypub/renderer/delete.ts3
-rw-r--r--src/remote/activitypub/renderer/undo.ts3
-rw-r--r--src/remote/activitypub/renderer/update.ts3
-rw-r--r--src/server/api/common/generate-muted-note-thread-query.ts17
-rw-r--r--src/server/api/endpoints/notes/mentions.ts2
-rw-r--r--src/server/api/endpoints/notes/state.ts28
-rw-r--r--src/server/api/endpoints/notes/thread-muting/create.ts54
-rw-r--r--src/server/api/endpoints/notes/thread-muting/delete.ts40
-rw-r--r--src/services/note/create.ts29
-rw-r--r--src/services/note/unread.ts11
-rw-r--r--test/thread-mute.ts103
-rw-r--r--test/utils.ts3
62 files changed, 916 insertions, 287 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index a29b31a045..0000000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-version: 2.1
-
-executors:
- docker:
- working_directory: /tmp/workspace
- docker:
- - image: docker:latest
-
-jobs:
- docker:
- parameters:
- with_deploy:
- type: boolean
- default: false
- executor: docker
- steps:
- - checkout
- - setup_remote_docker:
- version: 19.03.13
- - run:
- name: Build
- command: |
- docker build -t misskey/misskey .
- - when:
- condition: <<parameters.with_deploy>>
- steps:
- - run:
- name: Deploy
- command: |
- if [ "$DOCKERHUB_USERNAME$DOCKERHUB_PASSWORD" ]
- then
- apk update && apk add jq
- docker tag misskey/misskey misskey/misskey:$(cat package.json | jq -r .version)
- docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD
- docker push -a misskey/misskey
- else
- echo -e '\033[0;33mAborted deploying to Docker Hub\033[0;39m'
- fi
-
-workflows:
- version: 2
- docker:
- jobs:
- - docker:
- name: auto-build
- with_deploy: true
- filters:
- branches:
- only: master
diff --git a/.circleci/misskey/default.yml b/.circleci/misskey/default.yml
deleted file mode 100644
index ae18a841bd..0000000000
--- a/.circleci/misskey/default.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-url: 'http://misskey.local'
-port: 8080
-db:
- host: localhost
- port: 5432
- db: test-misskey
- user: postgres
- pass: ''
-redis:
- host: localhost
- port: 6379
-id: aid
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index 2a41c12c7e..0000000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1,38 +0,0 @@
-# PATH OWNERS
-/.autogen/ @acid-chicken
-/.circleci/ @syuilo @acid-chicken
-/.config/ @syuilo @AyaMorisawa @mei23 @acid-chicken @rinsuki
-# /.config/mongo_initdb_example.js @khws4v1
-/.github/ @syuilo @AyaMorisawa @acid-chicken
-/.vscode/ @acid-chicken
-/assets/ @syuilo # @tamaina
-/docs/ @syuilo
-/docs/*.en.md @AyaMorisawa # @skid9000
-# /docs/*.fr.md @BoFFire
-# /docs/docker.*.md @khws4v1
-/locales/ @syuilo
-/src/ @syuilo @AyaMorisawa @mei23 @acid-chicken @rinsuki
-# /src/crypto_key.cc @akihikodaki
-# /src/crypto_key.d.ts @akihikodaki
-/.dockerignore @syuilo # @khws4v1
-/.editorconfig @syuilo @AyaMorisawa
-/.eslintrc @syuilo
-/.gitattributes @syuilo
-/.gitignore @syuilo
-/.npmrc @syuilo
-/.vsls.json @AyaMorisawa
-/CHANGELOG.md @syuilo
-/CODE_OF_CONDUCT.md @syuilo
-/CONTRIBUTING.md @syuilo
-/Dockerfile @syuilo @AyaMorisawa @acid-chicken # @khws4v1
-/LICENSE @syuilo
-/README.md @syuilo @AyaMorisawa @acid-chicken # @nikhiljha
-# /binding.gyp @akihikodaki
-/crowdin.yml @syuilo
-# /docker-compose.yml @khws4v1
-/gulpfile.ts @syuilo @AyaMorisawa
-/jsconfig.json @syuilo @AyaMorisawa
-/package.json @syuilo @AyaMorisawa
-/tsconfig.json @syuilo @AyaMorisawa
-/tslint.json @syuilo @AyaMorisawa
-/webpack.config.ts @syuilo @AyaMorisawa
diff --git a/.circleci/misskey/test.yml b/.github/misskey/test.yml
index ae18a841bd..ae18a841bd 100644
--- a/.circleci/misskey/test.yml
+++ b/.github/misskey/test.yml
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000000..0b3bbc186f
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,21 @@
+name: Lint
+
+on:
+ push:
+ branches:
+ - master
+ - develop
+ pull_request:
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ submodules: true
+ - uses: actions/setup-node@v1
+ with:
+ node-version: 12.x
+ - run: yarn install
+ - run: yarn lint
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/test.yml
index a91572ad78..045d209802 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/test.yml
@@ -1,4 +1,5 @@
-name: Node.js CI
+name: Test
+
on:
push:
branches:
@@ -7,12 +8,12 @@ on:
pull_request:
jobs:
- build_and_test:
+ mocha:
runs-on: ubuntu-latest
strategy:
matrix:
- node-version: [14.x, 16.x]
+ node-version: [16.x]
services:
postgres:
@@ -44,16 +45,43 @@ jobs:
- name: Build
run: yarn build
- name: Test
- run: yarn test
+ run: yarn mocha
- lint:
+ e2e:
runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [16.x]
+
+ services:
+ postgres:
+ image: postgres:12.2-alpine
+ ports:
+ - 54312:5432
+ env:
+ POSTGRES_DB: test-misskey
+ POSTGRES_HOST_AUTH_METHOD: trust
+ redis:
+ image: redis:4.0-alpine
+ ports:
+ - 56312:6379
+
steps:
- uses: actions/checkout@v2
with:
submodules: true
- - uses: actions/setup-node@v1
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v1
with:
- node-version: 12.x
- - run: yarn install
- - run: yarn lint
+ node-version: ${{ matrix.node-version }}
+ - name: Install dependencies
+ run: yarn install
+ - name: Check yarn.lock
+ run: git diff --exit-code yarn.lock
+ - name: Copy Configure
+ run: cp test/test.yml .config
+ - name: Build
+ run: yarn build
+ - name: Test
+ run: yarn e2e
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13dccdc38b..6847049bb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,18 @@
-->
+## 12.95.0 (2021/10/31)
+
+### Improvements
+- スレッドミュート機能
+
+### Bugfixes
+- リレー向けのActivityが一部実装で除外されてしまうことがあるのを修正
+- 削除したノートやユーザーがリモートから参照されると復活することがあるのを修正
+- クライアント: ページ編集時のドロップダウンメニューなどが動作しない問題を修正
+- クライアント: コントロールパネルのカスタム絵文字タブが切り替わらないように見える問題を修正
+- API: ユーザー情報の hasUnreadChannel が常に false になっている問題を修正
+
## 12.94.1 (2021/10/25)
### Improvements
diff --git a/README.md b/README.md
index ce0aa09417..a60f71cacf 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
<div align="center">
-[![CircleCI](https://img.shields.io/circleci/project/github/misskey-dev/misskey.svg?style=for-the-badge&logo=circleci)](https://circleci.com/gh/misskey-dev/misskey)
[![Dependencies](https://img.shields.io/david/misskey-dev/misskey.svg?style=for-the-badge&logo=npm)](https://david-dm.org/misskey-dev/misskey)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge&logo=github)](http://makeapullrequest.com)
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
diff --git a/cypress.json b/cypress.json
index f2c02689e4..e858e480b0 100644
--- a/cypress.json
+++ b/cypress.json
@@ -1,3 +1,3 @@
{
- "baseUrl": "http://localhost"
+ "baseUrl": "http://localhost:61812"
}
diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js
index 182f70ff68..a754f41b98 100644
--- a/cypress/integration/basic.js
+++ b/cypress/integration/basic.js
@@ -128,7 +128,8 @@ describe('After user signup', () => {
cy.get('[data-cy-signin-username] input').type('alice');
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
- cy.contains('アカウントが凍結されています');
+ // TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
+ cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
});
});
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index d7f038393f..71626224a7 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -1,5 +1,6 @@
---
_lang_: "العربية"
+headlineMisskey: "شبكة مرتبطة بالملاحظات"
introMisskey: "اهلا بك! ميسكي هو منصة تدوين مصغر لا مركزية ومفتوحة المصدر.\nيمكنك مشاركة \"ملاحظات\" عن ما يجري حولك، وإخبار الجميع عن نفسك 📡\nتسمح لك \"الانفعالات\" بتعبير عن شعورك حول ملاحظات الآخرين 👍\nاكتشف عالمًا جديدًا 🚀"
monthAndDay: "{day}/{month}"
search: "البحث"
@@ -12,6 +13,7 @@ ok: " حسناً"
gotIt: "فهِمت"
cancel: " إلغاء"
enterUsername: "أدخِل إسم مسخدم"
+renotedBy: "أعاد {user} نشر ملاحظة"
noNotes: "لم يتم العثور على أية ملاحظات"
noNotifications: "ليس هناك أية اشعارات"
instance: "مثيل الخادم"
@@ -26,7 +28,7 @@ login: "لِج"
loggingIn: "جارٍ تسجيل الدخول"
logout: "الخروج"
signup: "أنشئ حسابًا"
-uploading: "عملية الإرسال جارية"
+uploading: "يرفع..."
save: "حفظ"
users: "المستخدمون"
addUser: "اضافة مستخدم"
@@ -37,7 +39,7 @@ favorited: "تمت الإضافة إلى المفضلة."
alreadyFavorited: "تمت إضافته بالفعل إلى المفضلة."
cantFavorite: "تعذرت الإضافة إلى المفضلة."
pin: "دبّسها على الصفحة الشخصية"
-unpin: "ألغ تثبيتها من ملفك الشخصي"
+unpin: "ألغ تدبيسها من ملفك الشخصي"
copyContent: "انسخ المحتوى"
copyLink: "انسخ الرابط"
delete: "حذف"
@@ -63,13 +65,14 @@ files: "الملفات"
download: "تنزيل"
driveFileDeleteConfirm: "أمتأكد من حذف ملف {name}؟ كل الملاحظات المُرفق بها هذا الملف ستحذف."
unfollowConfirm: "أمتأكد من إلغاء متابعة {name}؟"
+exportRequested: "قد تستغرق عملية التصدير بعض الوقت. بمجرد الانتهاء ستتم إضافة الملف الناتج إلى قرص التخزين."
importRequested: "يستغرق الاستيراد بعض الوقت"
lists: "القوائم"
noLists: "ليس لديك أية قائمة"
note: "ملاحظة"
notes: "الملاحظات"
following: "المتابَعون"
-followers: "المتابِعين"
+followers: "المتابِعون"
followsYou: "يتابعك"
createList: "إنشاء قائمة"
manageLists: "إدارة القوائم"
@@ -82,7 +85,7 @@ serverIsDead: "الخادم لا يستجيب، حاول بعد قليل"
youShouldUpgradeClient: "حدّث الصفحة لعرضها."
enterListName: "اسم القائمة"
privacy: "الخصوصية"
-makeFollowManuallyApprove: "القبول يدويا طلبات الإشتراك"
+makeFollowManuallyApprove: "قبول طلبات الإشتراك يدويا"
defaultNoteVisibility: "مدى الرؤية الافتراضي"
follow: "تابِع"
followRequest: "طلب اشتراك"
@@ -90,7 +93,11 @@ followRequests: "طلبات الإشتراك"
unfollow: "إلغاء الاشتراك"
followRequestPending: "طلبات الإشتراك المعلّقة"
enterEmoji: "أدخل إيموجي"
+renote: "أعد النشر"
unrenote: "إلغاء مشاركة الملاحظة"
+renoted: "أُعيد نشره"
+cantRenote: "لا يمكن إعادة نشر الملاحظة"
+cantReRenote: "لا يمكنك إعادة نشر ملاحظة معاد نشرها"
quote: "اقتبس"
pinnedNote: "ملاحظة مدبسة"
pinned: "دبّسها على الصفحة الشخصية"
@@ -99,9 +106,12 @@ clickToShow: "اضغط للعرض"
sensitive: "محتوى حساس"
add: "إضافة"
reaction: "تفاعل"
+reactionSettingDescription: "اختر التفاعلات المفضلة التي تريد تثبيتها في منتقي التفاعلات."
+reactionSettingDescription2: "اسحب لإعادة التنظيم ، انقر للحذف ، استخدم \"+\" للإضافة."
rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات"
attachCancel: "أزل المرفق"
markAsSensitive: "علّمه كمحتوى حساس"
+unmarkAsSensitive: "ألغ تعيينه كمحتوى حساس"
enterFileName: "ادخل اسم الملف"
mute: "اكتم"
unmute: "إلغاء الكتم"
@@ -111,8 +121,12 @@ suspend: "علِق"
unsuspend: "ألغ التعليق"
blockConfirm: "أمتأكد من حجب هذا الحساب؟"
unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟"
+suspendConfirm: "أمتأكد من تعليق الحساب؟"
+unsuspendConfirm: "أمتأكد من إلغاء تعليق؟"
selectList: "اختر قائمة"
selectAntenna: "اختر هوائيًا"
+selectWidget: "اختر ودجة"
+editWidgets: "عدّل الودجات"
editWidgetsExit: "تم"
customEmojis: "إيموجي مخصص"
emoji: "الوجوه التعبيرية"
@@ -122,6 +136,10 @@ emojiUrl: "رابط الوجه التعبيري"
addEmoji: "إضافة إيموجي"
settingGuide: "الإعدادات المستحسنة"
cacheRemoteFiles: "خزن مؤقتا الملفات البعيدة"
+flagAsBot: "علّمه كحساب آلي"
+flagAsBotDescription: "فعّل هذا الخيار إذا كان هذا الحساب يُدار عبر برمجية. إذا فُعل فسيكون بمثابة علامة للمطورين الآخرين لتجنب سلاسل لا متناعية من التفاعل بين حسابات الآلية وضبط أنظمة ميسكي للتعامل مع هذا الحساب كروبوت."
+flagAsCat: "علّم هذا الحساب كحساب قط"
+flagAsCatDescription: "فعّل هذا الخيار لوضع علامة على الحساب لتوضيح أنه حساب قط."
autoAcceptFollowed: "اقبل طلبات المتابعة تلقائيا من الحسابات المتابَعة"
addAccount: "أضف حساباً"
loginFailed: "فشل الولوج"
@@ -165,6 +183,9 @@ statistics: "الإحصائيات"
clearQueue: "تفريغ قائمة الإنتظار"
clearQueueConfirmTitle: "أتريد مسح الطابور؟"
clearCachedFiles: "امسح التخزين المؤقت"
+clearCachedFilesConfirm: "أتريد حذف التخزين المؤقت للملفات البعيدة؟"
+blockedInstances: "المثلاء المحجوبون"
+blockedInstancesDescription: "قائمة بالمثلاء التي تريد حظرها بحيث كل نطاق في سطر لوحده. بعد إدراجهم لن يتمكنوا من التفاعل مع هذا المثيل."
muteAndBlock: "تم كتمها / تم حجبها"
mutedUsers: "الحسابات التي تم كتمها"
blockedUsers: "الحسابات التي تم حظرها"
@@ -191,7 +212,7 @@ usernameOrUserId: "اسم المستخدم أو معرّفه"
noSuchUser: "لم يُعثَر على المستخدم"
lookup: "البحث"
announcements: "الإعلانات"
-imageUrl: "عنوان URL للصورة"
+imageUrl: "رابط الصورة"
remove: "حذف"
removed: "تم حذفه بنجاح"
removeAreYouSure: "متأكد من أنك تريد حذف {x}؟"
@@ -199,11 +220,11 @@ deleteAreYouSure: "متأكد من أنك تريد حذف {x}؟"
resetAreYouSure: "هل تريد إعادة التعيين؟"
saved: "تم حفظه"
messaging: "المحادثة"
-upload: "تحميل"
+upload: "ارفع"
fromDrive: "من المخزن"
-fromUrl: "من عنوان URL"
-uploadFromUrl: "التحميل عبر URL"
-uploadFromUrlDescription: "رابط الملف المراد تحميله "
+fromUrl: "عبر رابط"
+uploadFromUrl: "ارفع عبر رابط"
+uploadFromUrlDescription: "رابط الملف المراد رفعه"
uploadFromUrlRequested: "الرفع مطلوب"
uploadFromUrlMayTakeTime: "سيستغرق بعض الوقت لاتمام الرفع "
explore: "استكشاف"
@@ -248,7 +269,7 @@ unableToDelete: "لا يمكن حذفه"
inputNewFileName: "ادخل الإسم الجديد للملف"
inputNewFolderName: "ادخل الإسم الجديد للمجلد"
hasChildFilesOrFolders: "الان الملف غير فارغ. لا يمكن حذفه"
-copyUrl: "انسخ عنوان URL"
+copyUrl: "انسخ الرابط"
rename: "إعادة التسمية"
avatar: "الصورة الرمزية"
banner: "الصورة الرأسية"
@@ -267,7 +288,7 @@ instanceName: "اسم مثيل الخادم"
instanceDescription: "وصف مثيل الخادم"
maintainerName: "المدير"
maintainerEmail: "عنوان بريد المدير الإلكتروني"
-tosUrl: "عنوان URL لشروط الخدمة"
+tosUrl: "رابط صفحة شروط الخدمة"
thisYear: "هذا العام"
thisMonth: "هذا الشهر"
today: "اليوم"
@@ -289,10 +310,10 @@ iconUrl: "رابط الأيقونة"
bannerUrl: "رابط صورة اللافتة"
backgroundImageUrl: "رابط صورة الخلفية"
basicInfo: "المعلومات الأساسية "
-pinnedUsers: "المستخدمون المثبتون"
-pinnedUsersDescription: "قائمة المستخدمين المثبتين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده."
-pinnedPages: "الصفحات المثبتة"
-pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تثبيتها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده."
+pinnedUsers: "المستخدمون المدبسون"
+pinnedUsersDescription: "قائمة المستخدمين المدبسين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده."
+pinnedPages: "الصفحات المدبسة"
+pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تدبيسها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده."
pinnedNotes: "ملاحظة مدبسة"
hcaptchaSiteKey: "مفتاح الموقع"
hcaptchaSecretKey: "المفتاح السري"
@@ -310,9 +331,11 @@ withFileAntenna: "ملاحظات تحوي ملفات فقط"
caseSensitive: "حساسية حالة الأحرف"
withReplies: "بالردود"
notesAndReplies: "الملاحظات والردود"
-withFiles: "بالمرفقات"
+withFiles: "ذات مرفقات"
silence: "اكتم"
+silenceConfirm: "أمتأكد من كتم هذا المستخدم؟"
unsilence: "إلغاء الكتم"
+unsilenceConfirm: "أمتأكد من إلغاء كتم هذا المستخدم؟"
popularUsers: "المستخدمون الشائعون"
recentlyUpdatedUsers: "أصحاب النشاطات الأخيرة"
recentlyRegisteredUsers: "المستخدمون المنضمون حديثًا"
@@ -339,14 +362,20 @@ newPasswordIs: "كلمتك السرية الجديدة هي {password}"
reduceUiAnimation: "قلص تأثيرات الواجهة"
share: "شارِك"
notFound: "غير موجود"
+notFoundDescription: "تعذر العثور على صفحة يقود إليها هذا الرابط."
+uploadFolder: "المجلد الافتراضي للرفع"
cacheClear: "مسح ذاكرة التخزين المؤقت"
markAsReadAllNotifications: "وضع جميع الإشعارات كأنها مقروءة"
+markAsReadAllUnreadNotes: "علّم جميع الملاحظات كمقروءة"
+markAsReadAllTalkMessages: "علّم جميع الرسائل كمقروءة"
help: "المساعدة"
inputMessageHere: "اكتب رسالتك هنا"
close: "اغلق"
group: "الفريق"
groups: "الفِرَق"
createGroup: "انشئ فريقًا"
+ownedGroups: "مجموعات المالك"
+joinedGroups: "المجموعات المنضم إليها"
invites: "دعوة"
groupName: "اسم الفريق"
members: "الأعضاء"
@@ -360,13 +389,18 @@ next: "التالية"
retype: "أعد الكتابة"
noteOf: "ملاحظات {user}"
inviteToGroup: "دعوة إلى فريق"
+maxNoteTextLength: "حد عدد المحارف لكل ملاحظة"
+quoteAttached: "اِقتُبسَ"
noMessagesYet: "ليس هناك رسائل بعد"
newMessageExists: "لقد تلقيت رسالة جديدة"
+onlyOneFileCanBeAttached: "يمكنك إرفاق ملف واحد بالرسالة"
+signinRequired: "رجاءً لِج"
invitations: "دعوة"
invitationCode: "رمز الدعوة"
checking: "التحقق جارٍ"
available: "متوفر"
unavailable: "غير متوفر"
+usernameInvalidFormat: "يمكنك استخدام A-z، a-z، 0-9، _"
tooShort: "قصير جدًا"
tooLong: "طويل جدًا"
weakPassword: "الكلمة السرية ضعيفة"
@@ -375,11 +409,15 @@ strongPassword: "الكلمة السرية قوية"
passwordMatched: "التطابق صحيح!"
passwordNotMatched: "غير متطابقتان"
signinWith: "الولوج عبر {x}"
+signinFailed: "فشل الولوج، خطأ في اسم المستخدم أو كلمة المرور."
or: "أو"
+language: "اللغة"
uiLanguage: "لغة واجهة المستخدم"
+groupInvited: "دُعيت إلى مجموعة"
aboutX: "عن {x}"
useOsNativeEmojis: "استخدم الإيموجيات الخاصة بنظام التشغيل"
youHaveNoGroups: "لا تمتلك أية فِرَق"
+joinOrCreateGroup: "احصل على دعوة لمجموعة أو أنشئ واحدة."
noHistory: "السجل فارغ"
signinHistory: "تاريخ تسجيل الدخول"
doing: "انتظر لحظة"
@@ -387,8 +425,10 @@ category: "الفئات"
tags: "الوسوم"
docSource: "مصدر هذا المستند"
createAccount: "أنشئ حسابًا"
+existingAccount: "الحسابات الموجودة"
regenerate: "أعِد التوليد"
fontSize: "حجم الخط"
+noFollowRequests: "ليس لديك طلبات متابعة معلقة"
openImageInNewTab: "إفتح الصورة بصفحة جديدة"
dashboard: "لوحة التحكم"
local: "المحلي"
@@ -429,13 +469,17 @@ nothing: "لا يوجد شيء هنا"
lastUsedDate: "آخر استخدام"
state: "الحالة"
sort: "ترتيب حسب"
+ascendingOrder: "تصاعدي"
+descendingOrder: "تنازلي"
output: "الخارجة"
updateRemoteUser: "تحديث المعلومات عن المستخدم البعيد"
deleteAllFiles: "حذف كافة الملفات"
deleteAllFilesConfirm: "أتريد حذف كل الملفات؟"
-removeAllFollowing: "ألغ متابعة كل المتابِعين"
+removeAllFollowing: "ألغ متابعة كل المتابَعين"
userSuspended: "تم تعليق هذا المستخدم."
userSilenced: "تم إسكات هذا المستخدم."
+yourAccountSuspendedTitle: "هذا الحساب معلق"
+menu: "القائمة"
addItem: "إضافة عنصر"
rooms: "الغرفة"
relays: "المُرَحلات"
@@ -443,9 +487,15 @@ addRelay: "إضافة مُرحّل"
addedRelays: "المرحلات التي تم إضافتها"
deletedNote: "ملاحظة محذوفة"
invisibleNote: "ملاحظة مخفية"
+enableInfiniteScroll: "فعّل التمرير المتواصل"
+visibility: "الظهور"
poll: "استطلاع رأي"
useCw: "إخفاء المحتوى"
+enablePlayer: "افتح مشغل الفيديو"
+disablePlayer: "أغلق مشغل الفيديو"
themeEditor: "مصمم القوالب"
+description: "الوصف"
+leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟"
manage: "إدارة "
plugins: "الإضافات"
width: "العرض"
@@ -453,12 +503,14 @@ height: "الإرتفاع"
large: "كبير"
medium: "متوسط"
small: "صغير"
+generateAccessToken: "ولّد رمز الوصول"
permission: "أذونات"
enableAll: "تشغيل الكل"
disableAll: "تعطيل الكل"
tokenRequested: "منح حق الوصول إلى الحساب"
notificationType: "أنواع الإشعارات"
edit: "التعديل"
+emailServer: "خادم البريد الإلكتروني"
email: "البريد الإلكتروني "
emailAddress: "عنوان البريد الالكتروني"
smtpHost: "المضيف"
@@ -469,6 +521,12 @@ makeActive: "تفعيل"
display: "المظهر"
copy: "نسخ"
metrics: "المقاييس"
+channel: "القنوات"
+create: "أنشئ"
+notificationSetting: "إعدادات التنبيهات"
+notificationSettingDesc: "اختر نوع التنبيهات المراد عرضها"
+other: "منوعات"
+regenerateLoginToken: "أعد توليد الرمز"
fileIdOrUrl: "معرف الملف أو رابط"
chatOpenBehavior: "سلوك نفاذة المحادثة عند فتحها"
behavior: "السلوك"
@@ -476,7 +534,7 @@ sample: "مثال"
abuseReports: "البلاغات"
reportAbuse: "البلاغات"
reportAbuseOf: "أبلغ عن {name}"
-fillAbuseReportDescription: "أكتب بالتفصيل سبب الإبلاغ، إذا كنت تبلغ عن ملاحظة أرفق رابط لها."
+fillAbuseReportDescription: "أكتب بالتفصيل سبب البلاغ، إذا كنت تبلغ عن ملاحظة أرفق رابط لها."
abuseReported: "أُرسل البلاغ، شكرًا لك"
send: "أرسل"
abuseMarkAsResolved: "علّم البلاغ كمحلول"
@@ -494,7 +552,9 @@ manageAccessTokens: "إدارة رموز الوصول"
accountInfo: "معلومات الحساب"
notesCount: "عدد الملاحظات"
repliesCount: "عدد الردود المرسلة"
+renotesCount: "عدد الملاحظات المعاد نشرها (المرسلة)"
repliedCount: "عدد الردود المستلمة"
+renotedCount: "عدد الملاحظات المعاد نشرها (المستلمة)"
followingCount: "عدد الحسابات المتابَعة"
followersCount: "عدد المتابِعين"
sentReactionsCount: "عدد الانفعالات المرسلة"
@@ -503,33 +563,141 @@ pollVotesCount: "عدد الاستطلاعات المرسلة"
pollVotedCount: "عدد الاستطلاعات المستلمة"
yes: "نعم"
no: "لا"
+driveFilesCount: "عدد الملفات في قرص التخزين"
+useSystemFont: "استخدم الخط الافتراضية للنظام"
+experimentalFeatures: "ميّزات اختبارية"
+developer: "المطور"
clearCache: "امسح التخزين المؤقت"
currentVersion: "الإصدار الحالي"
latestVersion: "آخر نسخة مستقرة"
usageAmount: "الإستخدام"
capacity: "السعة"
inUse: "مستخدم"
+useReactionPickerForContextMenu: "افتح منتقي التفاعلات عند التقر بالزر الأيمن"
+typingUsers: "{users} يكتب(ون)..."
+jumpToSpecifiedDate: "انتقل إلى التاريخ المحدد"
+showingPastTimeline: "أنت تستعرض حاليًا خيطًا زمنيًا قديمًا"
+markAllAsRead: "علّم الكل كمقروء"
+goBack: "رجوع"
+unlikeConfirm: "أتريد إلغاء إعجابك؟"
+fullView: "ملء الشاشة"
+quitFullView: "اخرج من وضع ملء للشاشة"
+addDescription: "أضف وصفًا"
info: "عن"
+userInfo: "معلومات المستخدم"
+unknown: "مجهول"
+onlineStatus: "الحالة"
+hideOnlineStatus: "اخف الحالة"
+online: "متصل"
+active: "نشط"
+offline: "غير متصل"
+notRecommended: "غير مستحسن"
+botProtection: "الحماية من الحسابات الآلية"
+instanceBlocking: "المثيلات المحجوبة"
+selectAccount: "اختر حسابًا"
+enabled: "مفعّل"
+disabled: "معطّل"
+quickAction: "الإجراءات السّريعة"
user: "المستخدمون"
administration: "إدارة "
+accounts: "الحسابات"
+switch: "بدّل"
+noBotProtectionWarning: "لم تضبط الحماية من الحسابات الآلية"
+configure: "اضبط"
postToGallery: "انشر في المعرض"
gallery: "المعرض"
+recentPosts: "المشاركات الحديثة"
+shareWithNote: "شاركه في ملاحظة"
+ads: "الإعلانات"
expiration: "ينتهي استطلاع الرأي في"
+priority: "الأولوية"
+high: "عالية"
middle: "متوسط"
+low: "منخفضة"
+emailNotConfiguredWarning: "لم تعيّن بريدًا إلكترونيًا"
+ratio: "النسبة"
+previewNoteText: "اعرض معاينة"
+customCss: "CSS مخصصة"
global: "الشامل"
+squareAvatars: "اعرض شكل الصور الرمزية كمربعات"
sent: "أرسل"
+received: "اُستلم"
+searchResult: "نتائج البحث"
+hashtags: "الوسوم"
+learnMore: "راجع المزيد"
+misskeyUpdated: "حُدث ميسكي!"
+whatIsNew: "اعرض التغييرات"
+translate: "ترجم"
+translatedFrom: "تُرجم من {x}"
+accountDeletionInProgress: "حذف الحساب جارٍ"
+usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله."
+lastCommunication: "آخر تواصل"
+itsOn: "مفعّل"
+itsOff: "معطّل"
+emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
+filter: "رشّح"
+controlPanel: "لوحة التحكم"
+manageAccounts: "إدارة الحسابات"
+makeReactionsPublic: "اجعل سجل التفاعلات علنيًا"
+makeReactionsPublicDescription: "هذا سيجعل قائمة تفاعلاتك مرئية للعلن."
+classic: "تقليدي"
_docs:
admin: "إدارة "
+_gallery:
+ unlike: "أزل الإعجاب"
_email:
_follow:
title: "يتابعك"
+_registry:
+ keys: "المفاتيح"
+ domain: "النّطاق"
+ createKey: "أنشئ مفتاحًا"
+_aboutMisskey:
+ about: "ميسكي هو برمجية مفتوحة المصدر يطورها syuilo منذ 2014."
+ contributors: "المساهم الرئيسي"
+ allContributors: "كل المساهمين"
+ source: "الشفرة المصدرية"
+ translation: "ترجم ميسكي"
+ donate: "تبرع لميسكي"
+ morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا لكم 🥰"
+ patrons: "الداعمون"
+_nsfw:
+ force: "اخف كل الوسائط"
_mfm:
mention: "أشر الى"
+ hashtag: "الوسوم"
+ url: "الرابط"
+ urlDescription: "يمكن عرض الروابط"
+ link: "رابط"
+ bold: "عريض"
+ small: "صغير"
quote: "اقتبس"
emoji: "إيموجي مخصص"
search: "البحث"
_reversi:
+ gameSettings: "إعدادات اللعبة"
+ chooseBoard: "اختر اللوح"
+ blackOrWhite: "أسود/أبيض"
+ blackIs: "{name} سيلعب بالأسود"
+ botSettings: "خيارات الحسابات الآلية"
+ waitingBoth: "استعد"
+ ready: "جاهز"
+ cancelReady: "ألغ الجهوزية"
+ opponentTurn: "دور الخصم"
+ myTurn: "دورك"
+ turnOf: "دور {name}"
+ pastTurnOf: "دور {name}"
+ surrender: "استسلم"
+ drawn: "تعادل"
+ won: "فاز {name}"
+ black: "أسود"
+ white: "أبيض"
total: "المجموع"
+ turnCount: "الدور {count}"
+ myGames: "جولاتي"
+ allGames: "كل الجولات"
+ ended: "انتهت"
+ playing: "يُلعب الآن"
_channel:
featured: "المتداوَلة"
_menuDisplay:
@@ -539,11 +707,15 @@ _theme:
install: "تنصيب قالب"
manage: "إدارة القوالب"
code: "شيفرة القالب"
+ description: "الوصف"
installed: "تم تنصيب {name}"
make: "إنشاء قالب"
alpha: "الشفافية"
keys:
+ link: "رابط"
+ hashtag: "وسم"
mention: "أشر الى"
+ renote: "أعد النشر"
messageBg: "خلفية المحادثة"
_sfx:
note: "الملاحظات"
@@ -569,11 +741,35 @@ _time:
_tutorial:
title: "كيف تستخدم Misskey"
step1_1: "مرحبًا!"
+ step1_2: "تدعى هذه الصفحة 'الخيط الزمني' وهي تحوي ملاحظات الأشخاص الذي تتابعهم مرتبة حسب تاريخ نشرها."
+ step1_3: "خيطك الزمني فارغ حاليًا بما أنك لا تتابع أي شخص ولم تنشر أي ملاحظة."
+ step2_1: "لننهي إعداد ملفك الشخصي قبل كتابة ملاحظة أو متابعة أشخاص."
+ step3_1: "هل أنهيت إعداد حسابك؟"
+ step3_2: "إذا تاليًا للنشر ملاحظة. أنقر على أيقونة القلم في أعلى الشاشة"
+ step5_3: "لمتابعة مستخدمين ادخل ملفهم الشخصي بالنقر على صورتهم الشخصية ثم اضغط زر 'تابع'."
_2fa:
registerKey: "تسجيل مفتاح أمان جديد"
_permissions:
+ "read:account": "اعرض معلومات حسابك"
"write:account": "تعديل معلومات حسابك"
+ "read:blocks": "اعرض قائمة المستخدمين المحجوبين"
+ "write:blocks": "عدّل قائمة المستخدمين المحجوبين"
+ "read:drive": "تصفح قرص التخزين"
+ "write:drive": "احذف أو عدّل محتويات قرص التخزين"
+ "read:favorites": "اعرض المفضلة"
+ "write:favorites": "عدّل المفضلة"
"read:notifications": "اظهر الإشعارات"
+ "read:reactions": "اعرض تفاعلاتك"
+ "write:reactions": "عدّل تفاعلاتك"
+ "write:votes": "صوّت"
+ "read:pages": "اعرض صفحاتك"
+ "write:pages": "عدّل أو احذف صفحاتك"
+ "read:user-groups": "اعرض مجموعات المستخدمين"
+ "write:user-groups": "عدّل أو احذف مجموعات المستخدمين"
+ "read:gallery": "اعرض المعرض"
+ "write:gallery": "عدّل المعرض"
+_auth:
+ shareAccess: "أتريد التفويض لـ \"{name}\" بالوصول لحسابك؟"
_weekday:
sunday: "الأحد"
monday: "الإثنين"
@@ -624,39 +820,74 @@ _poll:
_visibility:
public: "للعامة"
home: "الرئيسي"
- followers: "المتابِعين"
+ followers: "المتابِعون"
specified: "مباشرة"
localOnly: "المحلي فقط"
_postForm:
replyPlaceholder: "رد على هذه الملاحظة…"
quotePlaceholder: "اقتبس هذه الملاحظة…"
+ channelPlaceholder: "انشر في قناة..."
+ _placeholders:
+ c: "ما الذي تفكر فيه؟"
+ d: "ما الذي تريد قوله؟"
+ e: "أكتب..."
_profile:
name: "الإسم"
username: "اسم المستخدم"
+ description: "السيرة"
youCanIncludeHashtags: "يمكنك أيضًا إضافة وسوم إلى نبذتك التعريفية."
+ metadata: "معلومات إضافية"
+ metadataEdit: "عدّل المعلومات الإضافية"
+ metadataDescription: "يُمكنك عرض 4 حقول معلومات في ملفك الشخصي"
+ metadataLabel: "التسمية"
+ metadataContent: "المحتوى"
+ changeAvatar: "غيّر الصورة الرمزية"
+ changeBanner: "غيّر اللافتة"
_exportOrImport:
allNotes: "كل الملاحظات"
followingList: "المتابَعون"
- muteList: "اكتم"
- blockingList: "احجب"
+ muteList: "المستخدمون المكتومون"
+ blockingList: "المستخدمون المحجوبون"
userLists: "القوائم"
_charts:
usersTotal: "مجموع عدد المستخدمين والمستخدمات"
activeUsers: "المستخدمون النشطون"
+ notesTotal: "إجمالي الملاحظات"
_timelines:
home: "الرئيسي"
local: "المحلي"
social: "الاجتماعي"
global: "الشامل"
_rooms:
+ leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟"
+ chooseImage: "اختر صورة"
+ roomType: "نوع الغرفة"
_roomType:
default: "افتراضي"
+ washitsu: "الأسلوب الياباني"
_furnitures:
+ milk: "علبة حليب"
+ bed: "سرير"
+ low-table: "طاولة قصيرة"
+ desk: "مكتب"
+ chair: "كرسي"
+ chair2: "كرسي 2"
+ pc: "حاسوب"
monitor: "شاشة التحكم"
banknote: "أوراق نقدية"
_pages:
+ viewPage: "اعرض صفحاتك"
+ like: "أعجبني"
+ unlike: "أزل الإعجاب"
+ my: "صفحاتي"
blocks:
image: "الصور"
+ _post:
+ text: "المحتوى"
+ _button:
+ _action:
+ _dialog:
+ content: "المحتوى"
script:
categories:
list: "القوائم"
@@ -685,9 +916,11 @@ _notification:
youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}"
youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}"
youWereFollowed: "يتابعك"
+ youWereInvitedToGroup: "دُعيت إلى مجموعة"
_types:
follow: "المتابَعون"
mention: "أشر الى"
+ renote: "أعد النشر"
quote: "اقتبس"
reaction: "تفاعل"
_deck:
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 81fb263a7f..311500e85a 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -799,6 +799,8 @@ manageAccounts: "Benutzerkonten verwalten"
makeReactionsPublic: "Reaktionsverlauf veröffentlichen"
makeReactionsPublicDescription: "Jeder wird die Liste deiner gesendeten Reaktionen einsehen können."
classic: "Classic"
+muteThread: "Thread stummschalten"
+unmuteThread: "Threadstummschaltung aufheben"
_signup:
almostThere: "Fast geschafft"
emailAddressInfo: "Bitte gib deine Email-Adresse ein."
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 6d2961ce3d..36d0a48ea6 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -800,6 +800,8 @@ manageAccounts: "Manage Accounts"
makeReactionsPublic: "Set reaction history to public"
makeReactionsPublicDescription: "This will make the list of all your past reactions publicly visible."
classic: "Classic"
+muteThread: "Mute thread"
+unmuteThread: "Unmute thread"
_signup:
almostThere: "Almost there"
emailAddressInfo: "Please enter your email address."
diff --git a/locales/eo-UY.yml b/locales/eo-UY.yml
index 0733f0e475..88bf7ab2f4 100644
--- a/locales/eo-UY.yml
+++ b/locales/eo-UY.yml
@@ -543,6 +543,7 @@ learnMore: "Lernu pli"
translate: "Traduki"
translatedFrom: "Tradukita el {x}"
controlPanel: "Ŝaltpodio"
+classic: "Klasika"
_docs:
continueReading: "Legi plu"
features: "Funkcioj"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index dbb0bf1664..1326369f83 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -800,6 +800,8 @@ manageAccounts: "アカウントを管理"
makeReactionsPublic: "リアクション一覧を公開する"
makeReactionsPublicDescription: "あなたがしたリアクション一覧を誰でも見れるようにします。"
classic: "クラシック"
+muteThread: "スレッドをミュート"
+unmuteThread: "スレッドのミュートを解除"
_signup:
almostThere: "ほとんど完了です"
diff --git a/migration/1635500777168-note-thread-mute.ts b/migration/1635500777168-note-thread-mute.ts
new file mode 100644
index 0000000000..aed10d18d7
--- /dev/null
+++ b/migration/1635500777168-note-thread-mute.ts
@@ -0,0 +1,26 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class noteThreadMute1635500777168 implements MigrationInterface {
+ name = 'noteThreadMute1635500777168'
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`CREATE TABLE "note_thread_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "threadId" character varying(256) NOT NULL, CONSTRAINT "PK_ec5936d94d1a0369646d12a3a47" PRIMARY KEY ("id"))`);
+ await queryRunner.query(`CREATE INDEX "IDX_29c11c7deb06615076f8c95b80" ON "note_thread_muting" ("userId") `);
+ await queryRunner.query(`CREATE INDEX "IDX_c426394644267453e76f036926" ON "note_thread_muting" ("threadId") `);
+ await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ae7aab18a2641d3e5f25e0c4ea" ON "note_thread_muting" ("userId", "threadId") `);
+ await queryRunner.query(`ALTER TABLE "note" ADD "threadId" character varying(256)`);
+ await queryRunner.query(`CREATE INDEX "IDX_d4ebdef929896d6dc4a3c5bb48" ON "note" ("threadId") `);
+ await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD CONSTRAINT "FK_29c11c7deb06615076f8c95b80a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP CONSTRAINT "FK_29c11c7deb06615076f8c95b80a"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d4ebdef929896d6dc4a3c5bb48"`);
+ await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "threadId"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ae7aab18a2641d3e5f25e0c4ea"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c426394644267453e76f036926"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_29c11c7deb06615076f8c95b80"`);
+ await queryRunner.query(`DROP TABLE "note_thread_muting"`);
+ }
+
+}
diff --git a/package.json b/package.json
index f0bdd0049d..8687223923 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "12.94.1",
+ "version": "12.95.0",
"codename": "indigo",
"repository": {
"type": "git",
@@ -28,8 +28,9 @@
"lint": "tslint 'src/**/*.ts'",
"cy:open": "cypress open",
"cy:run": "cypress run",
- "e2e": "start-server-and-test start:test http://localhost cy:run",
- "test": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
+ "e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
+ "mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
+ "test": "npm run mocha",
"format": "gulp format"
},
"resolutions": {
diff --git a/src/client/components/forgot-password.vue b/src/client/components/forgot-password.vue
index cb2380f483..7fcf9aa720 100644
--- a/src/client/components/forgot-password.vue
+++ b/src/client/components/forgot-password.vue
@@ -7,21 +7,21 @@
>
<template #header>{{ $ts.forgotPassword }}</template>
- <form class="_monolithic_" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
- <div class="_section">
- <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
+ <form class="bafeceda" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
+ <div class="main _formRoot">
+ <MkInput class="_formBlock" v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
<template #label>{{ $ts.username }}</template>
<template #prefix>@</template>
</MkInput>
- <MkInput v-model="email" type="email" spellcheck="false" required>
+ <MkInput class="_formBlock" v-model="email" type="email" spellcheck="false" required>
<template #label>{{ $ts.emailAddress }}</template>
<template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
</MkInput>
- <MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
+ <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
</div>
- <div class="_section">
+ <div class="sub">
<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
</div>
</form>
@@ -69,3 +69,16 @@ export default defineComponent({
}
});
</script>
+
+<style lang="scss" scoped>
+.bafeceda {
+ > .main {
+ padding: 24px;
+ }
+
+ > .sub {
+ border-top: solid 0.5px var(--divider);
+ padding: 24px;
+ }
+}
+</style>
diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue
index 9efaf02697..363b3515fa 100644
--- a/src/client/components/form/select.vue
+++ b/src/client/components/form/select.vue
@@ -150,26 +150,26 @@ export default defineComponent({
});
};
- for (const optionOrOptgroup of options) {
- if (optionOrOptgroup.type === 'optgroup') {
- const optgroup = optionOrOptgroup;
- menu.push({
- type: 'label',
- text: optgroup.props.label,
- });
- for (const option of optgroup.children) {
+ const scanOptions = (options: VNode[]) => {
+ for (const vnode of options) {
+ if (vnode.type === 'optgroup') {
+ const optgroup = vnode;
+ menu.push({
+ type: 'label',
+ text: optgroup.props.label,
+ });
+ scanOptions(optgroup.children);
+ } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
+ const fragment = vnode;
+ scanOptions(fragment.children);
+ } else {
+ const option = vnode;
pushOption(option);
}
- } else if (Array.isArray(optionOrOptgroup.children)) { // 何故かフラグメントになってくることがある
- const fragment = optionOrOptgroup;
- for (const option of fragment.children) {
- pushOption(option);
- }
- } else {
- const option = optionOrOptgroup;
- pushOption(option);
}
- }
+ };
+
+ scanOptions(options);
os.popupMenu(menu, container.value, {
width: container.value.offsetWidth,
diff --git a/src/client/components/google.vue b/src/client/components/google.vue
index 6d8ae0b5bf..be724f038d 100644
--- a/src/client/components/google.vue
+++ b/src/client/components/google.vue
@@ -10,7 +10,12 @@ import { defineComponent } from 'vue';
import * as os from '@client/os';
export default defineComponent({
- props: ['q'],
+ props: {
+ q: {
+ type: String,
+ required: true,
+ }
+ },
data() {
return {
query: null,
@@ -21,10 +26,7 @@ export default defineComponent({
},
methods: {
search() {
- const engine = this.$store.state.webSearchEngine ||
- 'https://www.google.com/search?q={{query}}';
- const url = engine.replace('{{query}}', this.query)
- window.open(url, '_blank');
+ window.open(`https://www.google.com/search?q=${this.query}`, '_blank');
}
}
});
diff --git a/src/client/components/media-video.vue b/src/client/components/media-video.vue
index 44367ee999..4d4a551653 100644
--- a/src/client/components/media-video.vue
+++ b/src/client/components/media-video.vue
@@ -11,6 +11,7 @@
:title="video.name"
preload="none"
controls
+ @contextmenu.stop
>
<source
:src="video.url"
diff --git a/src/client/components/mention.vue b/src/client/components/mention.vue
index 101a9020ee..da13dcdddd 100644
--- a/src/client/components/mention.vue
+++ b/src/client/components/mention.vue
@@ -1,13 +1,12 @@
<template>
-<MkA class="ldlomzub" :class="{ isMe }" :to="url" v-user-preview="canonical" v-if="url.startsWith('/')">
- <span class="me" v-if="isMe">{{ $ts.you }}</span>
+<MkA v-if="url.startsWith('/')" class="ldlomzub" :class="{ isMe }" :to="url" v-user-preview="canonical" :style="{ background: bg }">
<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
<span class="main">
<span class="username">@{{ username }}</span>
<span class="host" v-if="(host != localHost) || $store.state.showFullAcct">@{{ toUnicode(host) }}</span>
</span>
</MkA>
-<a class="ldlomzub" :href="url" target="_blank" rel="noopener" v-else>
+<a v-else class="ldlomzub" :href="url" target="_blank" rel="noopener" :style="{ background: bg }">
<span class="main">
<span class="username">@{{ username }}</span>
<span class="host">@{{ toUnicode(host) }}</span>
@@ -17,10 +16,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import { toUnicode } from 'punycode/';
+import * as tinycolor from 'tinycolor2';
+import { toUnicode } from 'punycode';
import { host as localHost } from '@client/config';
import { wellKnownServices } from '../../well-known-services';
-import * as os from '@client/os';
+import { $i } from '@client/account';
export default defineComponent({
props: {
@@ -33,53 +33,46 @@ export default defineComponent({
required: true
}
},
- data() {
+
+ setup(props) {
+ const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
+
+ const wellKnown = wellKnownServices.find(x => x[0] === props.host);
+ const url = wellKnown ? wellKnown[1](props.username) : `/${canonical}`;
+
+ const isMe = $i && (
+ `@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
+ );
+
+ const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
+ bg.setAlpha(0.20);
+
return {
- localHost
+ localHost,
+ isMe,
+ url,
+ canonical,
+ toUnicode,
+ bg: bg.toRgbString(),
};
},
- computed: {
- url(): string {
- const wellKnown = wellKnownServices.find(x => x[0] === this.host);
- if (wellKnown) {
- return wellKnown[1](this.username);
- } else {
- return `/${this.canonical}`;
- }
- },
- canonical(): string {
- return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`;
- },
- isMe(): boolean {
- return this.$i && (
- `@${this.username}@${toUnicode(this.host)}` === `@${this.$i.username}@${toUnicode(localHost)}`.toLowerCase()
- );
- }
- },
- methods: {
- toUnicode
- }
});
</script>
<style lang="scss" scoped>
.ldlomzub {
+ display: inline-block;
+ padding: 4px 8px 4px 4px;
+ border-radius: 999px;
color: var(--mention);
&.isMe {
color: var(--mentionMe);
}
-
- > .me {
- pointer-events: none;
- user-select: none;
- font-size: 70%;
- vertical-align: top;
- }
> .icon {
width: 1.5em;
- margin: 0 0.2em;
+ margin: 0 0.2em 0 0;
vertical-align: bottom;
border-radius: 100%;
}
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index 40b0a68c58..568a2360d1 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -601,6 +601,12 @@ export default defineComponent({
});
},
+ toggleThreadMute(mute: boolean) {
+ os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+ noteId: this.appearNote.id
+ });
+ },
+
getMenu() {
let menu;
if (this.$i) {
@@ -657,6 +663,15 @@ export default defineComponent({
text: this.$ts.watch,
action: () => this.toggleWatch(true)
}) : undefined,
+ statePromise.then(state => state.isMutedThread ? {
+ icon: 'fas fa-comment-slash',
+ text: this.$ts.unmuteThread,
+ action: () => this.toggleThreadMute(false)
+ } : {
+ icon: 'fas fa-comment-slash',
+ text: this.$ts.muteThread,
+ action: () => this.toggleThreadMute(true)
+ }),
this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
icon: 'fas fa-thumbtack',
text: this.$ts.unpin,
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 91a3e3b87d..681e819a22 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -576,6 +576,12 @@ export default defineComponent({
});
},
+ toggleThreadMute(mute: boolean) {
+ os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+ noteId: this.appearNote.id
+ });
+ },
+
getMenu() {
let menu;
if (this.$i) {
@@ -632,6 +638,15 @@ export default defineComponent({
text: this.$ts.watch,
action: () => this.toggleWatch(true)
}) : undefined,
+ statePromise.then(state => state.isMutedThread ? {
+ icon: 'fas fa-comment-slash',
+ text: this.$ts.unmuteThread,
+ action: () => this.toggleThreadMute(false)
+ } : {
+ icon: 'fas fa-comment-slash',
+ text: this.$ts.muteThread,
+ action: () => this.toggleThreadMute(true)
+ }),
this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
icon: 'fas fa-thumbtack',
text: this.$ts.unpin,
diff --git a/src/client/init.ts b/src/client/init.ts
index 123d4020e1..654e176398 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -37,6 +37,8 @@ import { isMobile } from '@client/scripts/is-mobile';
import { initializeSw } from '@client/scripts/initialize-sw';
import { reloadChannel } from '@client/scripts/unison-reload';
import { reactionPicker } from '@client/scripts/reaction-picker';
+import { getUrlWithoutLoginId } from '@client/scripts/login-id';
+import { getAccountFromId } from '@client/scripts/get-account-from-id';
console.info(`Misskey v${version}`);
@@ -116,6 +118,25 @@ const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
+//#region loginId
+const params = new URLSearchParams(location.search);
+const loginId = params.get('loginId');
+
+if (loginId) {
+ const target = getUrlWithoutLoginId(location.href);
+
+ if (!$i || $i.id !== loginId) {
+ const account = await getAccountFromId(loginId);
+ if (account) {
+ await login(account.token, target);
+ }
+ }
+
+ history.replaceState({ misskey: 'loginId' }, '', target);
+}
+
+//#endregion
+
//#region Fetch user
if ($i && $i.token) {
if (_DEV_) {
diff --git a/src/client/pages/admin/emojis.vue b/src/client/pages/admin/emojis.vue
index 80e0e00ba9..5876db349e 100644
--- a/src/client/pages/admin/emojis.vue
+++ b/src/client/pages/admin/emojis.vue
@@ -48,7 +48,7 @@
</template>
<script lang="ts">
-import { computed, defineComponent } from 'vue';
+import { computed, defineComponent, toRef } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/form/input.vue';
import MkPagination from '@client/components/ui/pagination.vue';
@@ -112,7 +112,7 @@ export default defineComponent({
},
async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
+ this.$emit('info', toRef(this, symbols.PAGE_INFO));
},
methods: {
@@ -168,6 +168,10 @@ export default defineComponent({
<style lang="scss" scoped>
.ogwlenmc {
> .local {
+ .empty {
+ margin: var(--margin);
+ }
+
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
@@ -210,6 +214,10 @@ export default defineComponent({
}
> .remote {
+ .empty {
+ margin: var(--margin);
+ }
+
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
diff --git a/src/client/pages/admin/index.vue b/src/client/pages/admin/index.vue
index cda3ab912a..28157ff05a 100644
--- a/src/client/pages/admin/index.vue
+++ b/src/client/pages/admin/index.vue
@@ -26,7 +26,7 @@
</template>
<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
+import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { i18n } from '@client/i18n';
import MkSuperMenu from '@client/components/ui/super-menu.vue';
import FormGroup from '@client/components/debobigego/group.vue';
@@ -73,7 +73,13 @@ export default defineComponent({
const view = ref(null);
const el = ref(null);
const onInfo = (viewInfo) => {
- childInfo.value = viewInfo;
+ if (isRef(viewInfo)) {
+ watch(viewInfo, () => {
+ childInfo.value = viewInfo.value;
+ }, { immediate: true });
+ } else {
+ childInfo.value = viewInfo;
+ }
};
const pageProps = ref({});
diff --git a/src/client/scripts/login-id.ts b/src/client/scripts/login-id.ts
new file mode 100644
index 0000000000..0f9c6be4a9
--- /dev/null
+++ b/src/client/scripts/login-id.ts
@@ -0,0 +1,11 @@
+export function getUrlWithLoginId(url: string, loginId: string) {
+ const u = new URL(url, origin);
+ u.searchParams.append('loginId', loginId);
+ return u.toString();
+}
+
+export function getUrlWithoutLoginId(url: string) {
+ const u = new URL(url);
+ u.searchParams.delete('loginId');
+ return u.toString();
+}
diff --git a/src/client/themes/l-light.json5 b/src/client/themes/l-light.json5
index 79176cdd62..248355c945 100644
--- a/src/client/themes/l-light.json5
+++ b/src/client/themes/l-light.json5
@@ -15,5 +15,6 @@
navBg: '#fff',
panel: '#fff',
panelHeaderDivider: '@divider',
+ mentionMe: 'rgb(0, 179, 70)',
},
}
diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue
index ece80e60d9..cd78b6ae46 100644
--- a/src/client/ui/_common_/sidebar.vue
+++ b/src/client/ui/_common_/sidebar.vue
@@ -35,7 +35,7 @@
<MkA class="item" active-class="active" to="/settings" v-click-anime>
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
</MkA>
- <button class="item _button post" @click="post">
+ <button class="item _button post" @click="post" data-cy-open-post-form>
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
</button>
</div>
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 4f4047b613..f52c2ab722 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -17,6 +17,7 @@ import { PollVote } from '@/models/entities/poll-vote';
import { Note } from '@/models/entities/note';
import { NoteReaction } from '@/models/entities/note-reaction';
import { NoteWatching } from '@/models/entities/note-watching';
+import { NoteThreadMuting } from '@/models/entities/note-thread-muting';
import { NoteUnread } from '@/models/entities/note-unread';
import { Notification } from '@/models/entities/notification';
import { Meta } from '@/models/entities/meta';
@@ -138,6 +139,7 @@ export const entities = [
NoteFavorite,
NoteReaction,
NoteWatching,
+ NoteThreadMuting,
NoteUnread,
Page,
PageLike,
diff --git a/src/docs/ar-SA/advanced/share-page.md b/src/docs/ar-SA/advanced/share-page.md
index b5b4edbebb..57485b66be 100644
--- a/src/docs/ar-SA/advanced/share-page.md
+++ b/src/docs/ar-SA/advanced/share-page.md
@@ -33,7 +33,7 @@
<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
</dl>
-### 公開範囲
+### الظهور
※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
<dl>
diff --git a/src/docs/ar-SA/advanced/stream.md b/src/docs/ar-SA/advanced/stream.md
index 0e5edd2b0c..a74aafb633 100644
--- a/src/docs/ar-SA/advanced/stream.md
+++ b/src/docs/ar-SA/advanced/stream.md
@@ -29,7 +29,7 @@
**ストリームでのやり取りはすべてJSONです。**
-## チャンネル
+## القنوات
MisskeyのストリーミングAPIにはチャンネルという概念があります。これは、送受信する情報を分離するための仕組みです。 Misskeyのストリームに接続しただけでは、まだリアルタイムでタイムラインの投稿を受信したりはできません。 ストリーム上でチャンネルに接続することで、様々な情報を受け取ったり情報を送信したりすることができるようになります。
ひとつのストリーム上で、同時に複数のチャンネルに接続することができます。
diff --git a/src/docs/ar-SA/features/note.md b/src/docs/ar-SA/features/note.md
index 93e9ee0319..b9dcb13c33 100644
--- a/src/docs/ar-SA/features/note.md
+++ b/src/docs/ar-SA/features/note.md
@@ -12,7 +12,7 @@
<div class="info">ℹ️ コンピューターのクリップボードに画像データがある状態で、フォーム内のテキストボックスにペーストするとその画像を添付することができます。</div>
<div class="info">ℹ️ テキストボックス内で<kbd class="key">Ctrl + Enter</kbd>を押すことでも投稿できます。</div>
-## Renote
+## أعد النشر
既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノートをRenoteと呼びます。 自分がフォローしているユーザーの、気に入ったノートを自分のフォロワーに共有したい場合や、過去の自分のノートを再度共有したい場合に使います。 同じノートに対して無制限にRenoteを行うことができますが、あまり連続して使用すると迷惑になる場合もあるので、注意しましょう。
<div class="warn">⚠️ 公開範囲がフォロワーやダイレクトのノートはRenoteできません</div>
@@ -21,7 +21,7 @@ Renoteを削除するには、Renoteの時刻表示の隣にある「...」を
## CW
Contents Warningの略で、ノートの内容を、閲覧者の操作なしには表示しないようにできる機能です。主に長大な内容を隠すためや、ネタバレ防止などに使うことができます。 設定するには、フォームの「内容を隠す」ボタン(目のアイコン)を押します。すると新しい入力エリアが表れるので、そこに内容の要約を記入します。
-## 公開範囲
+## الظهور
ノートごとに、そのノートが公開される範囲を設定することができます。フォームの「ノート」ボタンの左にあるアイコンを押すと公開範囲を以下から選択できます。
### للعامة
diff --git a/src/docs/ar-SA/general/glossary.md b/src/docs/ar-SA/general/glossary.md
index 7ca515cf1f..0534c8d7c6 100644
--- a/src/docs/ar-SA/general/glossary.md
+++ b/src/docs/ar-SA/general/glossary.md
@@ -34,7 +34,7 @@ Misskeyに関する用語集です。
## NSFW
(読み: のっとせーふふぉーわーく) Not Safe For Workの略。画像を「閲覧注意」扱いにし、操作なしには表示しないようにすることができる機能。
-## Renote
+## أعد النشر
(読み: りのーと) 既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノート。詳細は[こちら。](../features/note)
## STL
diff --git a/src/docs/ar-SA/general/links.md b/src/docs/ar-SA/general/links.md
index 5ce5e1ddbb..2943015c92 100644
--- a/src/docs/ar-SA/general/links.md
+++ b/src/docs/ar-SA/general/links.md
@@ -4,7 +4,7 @@
- [Official Discord](https://discord.gg/Wp8gVStHW3) - Misskey公式Discordサーバー
- [Misskey Forum](https://forum.misskey.io/) - Misskeyに関する話題を扱うフォーラム
-## アカウント
+## الحسابات
- [@repo@misskey.io](https://misskey.io/@repo) - Misskeyのリポジトリの更新を投稿するbot
## ライブラリ
diff --git a/src/docs/en-US/features/keyboard-shortcut.md b/src/docs/en-US/features/keyboard-shortcut.md
index d59c447647..75018e81f2 100644
--- a/src/docs/en-US/features/keyboard-shortcut.md
+++ b/src/docs/en-US/features/keyboard-shortcut.md
@@ -1,24 +1,24 @@
-# キーボードショートカット
+# Keyboard shortcuts
## Global
-これらのショートカットは基本的にどこでも使えます。
+These shortcuts are usually available anywhere.
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Shortcut</th><th>Effect</th><th>Why this key?</th></tr>
</thead>
<tbody>
- <tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
- <tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
- <tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
+ <tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>Create a note</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
+ <tr><td><kbd class="key">T</kbd></td><td>Focus the latest note</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
+ <tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>Show/hide notifications</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>Search</td><td><b>S</b>earch</td></tr>
- <tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
+ <tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>Show help</td><td><b>H</b>elp</td></tr>
</tbody>
</table>
## 投稿にフォーカスされた状態
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Shortcut</th><th>Effect</th><th>Why this key?</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>上の投稿にフォーカスを移動</td><td>-</td></tr>
@@ -39,12 +39,12 @@
## Renoteフォーム
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Shortcut</th><th>Effect</th><th>Why this key?</th></tr>
</thead>
<tbody>
- <tr><td><kbd class="key">Enter</kbd></td><td>Renoteする</td><td>-</td></tr>
- <tr><td><kbd class="key">Q</kbd></td><td>フォームを展開する</td><td><b>Q</b>uote</td></tr>
- <tr><td><kbd class="key">Esc</kbd></td><td>フォームを閉じる</td><td>-</td></tr>
+ <tr><td><kbd class="key">Enter</kbd></td><td>Renote</td><td>-</td></tr>
+ <tr><td><kbd class="key">Q</kbd></td><td>Expand form</td><td><b>Q</b>uote</td></tr>
+ <tr><td><kbd class="key">Esc</kbd></td><td>Close form</td><td>-</td></tr>
</tbody>
</table>
@@ -52,7 +52,7 @@
デフォルトで「👍」にフォーカスが当たっている状態です。
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Shortcut</th><th>Effect</th><th>Why this key?</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd></td><td>上のリアクションにフォーカスを移動</td><td>-</td></tr>
diff --git a/src/docs/en-US/features/pages.md b/src/docs/en-US/features/pages.md
index 7fc6ee29c3..dc5134b940 100644
--- a/src/docs/en-US/features/pages.md
+++ b/src/docs/en-US/features/pages.md
@@ -1,9 +1,9 @@
# Pages
## Variables
-変数を使うことで動的なページを作成できます。テキスト内で <b>{ 変数名 }</b> と書くとそこに変数の値を埋め込めます。例えば <b>Hello { thing } world!</b> というテキストで、変数(thing)の値が <b>ai</b> だった場合、テキストは <b>Hello ai world!</b> になります。
+Use variables to create dynamic pages. Put <b>{ variable-name }</b> in your content to embed the value. For example, if a variable named "thing" has the value <b>ai</b>, the string <b>Hello { thing } world!</b> turns into <b>Hello ai world!</b>.
-変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から <b>A、B、C</b> と3つの変数を定義したとき、<b>C</b>の中で<b>A</b>や<b>B</b>を参照することはできますが、<b>A</b>の中で<b>B</b>や<b>C</b>を参照することはできません。
+Variables are evaluated from top to bottom, so referencing variables before declaring is not possible. For example, when declaring the three variables <b>A, B, C</b> in this order, you can use <b>A</b> or <b>B</b> in <b>C</b>, but you cannot use <b>B</b> or <b>C</b> in <b>A</b>.
ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。
diff --git a/src/docs/en-US/features/theme.md b/src/docs/en-US/features/theme.md
index a0a9b0d02f..e9b3406c4c 100644
--- a/src/docs/en-US/features/theme.md
+++ b/src/docs/en-US/features/theme.md
@@ -2,10 +2,10 @@
テーマを設定して、Misskeyクライアントの見た目を変更できます。
-## テーマの設定
+## Configuring the theme
設定 > テーマ
-## テーマを作成する
+## Creating a theme
テーマコードはJSON5で記述されたテーマオブジェクトです。 テーマは以下のようなオブジェクトです。
``` js
{
@@ -33,13 +33,13 @@
```
-* `id` ... テーマの一意なID。UUIDをおすすめします。
-* `name` ... テーマ名
-* `author` ... テーマの作者
-* `desc` ... テーマの説明(オプション)
+* `id` ... A unique theme ID. UUID Recommended.
+* `name` ... Theme name
+* `author` ... The author of the theme (you!)
+* `desc` ... The description of the theme (optional)
* `base` ... 明るいテーマか、暗いテーマか
* `light`にすると明るいテーマになり、`dark`にすると暗いテーマになります。
- * テーマはここで設定されたベーステーマを継承します。
+ * The theme will be inheriting the default values of the theme specified here.
* `props` ... テーマのスタイル定義。これから説明します。
### テーマのスタイル定義
diff --git a/src/docs/en-US/features/timeline.md b/src/docs/en-US/features/timeline.md
index ed62a44667..a98d2b8a29 100644
--- a/src/docs/en-US/features/timeline.md
+++ b/src/docs/en-US/features/timeline.md
@@ -1,31 +1,31 @@
# Timeline
-タイムラインは、[ノート](./note)が時系列で表示される機能です。 タイムラインには以下で示す種類があり、種類によって表示されるノートも異なります。 なお、タイムラインの種類によってはサーバーにより無効になっている場合があります。
+[Notes](./note) are shown in the timelines. There are several kinds of timelines as mentioned below and each of them displays the different set of notes. Servers might disable some of them.
## Home
-自分のフォローしているユーザーの投稿が流れます。HTLと略されます。
+This is where you see posts from users you follow. Often abbreviated as HTL.
## Local
-全てのローカルユーザーの「ホーム」指定されていない投稿が流れます。LTLと略されます。
+This is where you see all the posts from the local users, except those with "Home" visibility. Often abbreviated as LTL.
## Social
-自分のフォローしているユーザーの投稿と、全てのローカルユーザーの「ホーム」指定されていない投稿が流れます。STLと略されます。
+This is where you see the posts from users you follow AND all the posts from the local users, except those with "Home" visibility. Often abbreviated as STL.
## Global
-全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿が流れます。GTLと略されます。
+This is where you see the posts from the local users and the remote users in federated servers, except those with "Home" visibility. Often abbreviated as GTL.
## Comparison
-| ソース | | | Timeline | | |
-| ------------ | ----------- | ---- | -------- | ------ | ------ |
-| Users | Visiblility | Home | Local | Social | Global |
-| ローカル (フォロー) | Publish | ✔ | ✔ | ✔ | ✔ |
-| | Home | ✔ | | ✔ | |
-| | Followers | ✔ | ✔ | ✔ | ✔ |
-| リモート (フォロー) | Publish | ✔ | | ✔ | ✔ |
-| | Home | ✔ | | ✔ | |
-| | Followers | ✔ | | ✔ | ✔ |
-| ローカル (未フォロー) | Publish | | ✔ | ✔ | ✔ |
-| | Home | | | | |
-| | Followers | | | | |
-| リモート (未フォロー) | Publish | | | | ✔ |
-| | Home | | | | |
-| | Followers | | | | |
+| Source | | | Timeline | | |
+| ----------------------------- | ----------- | ---- | -------- | ------ | ------ |
+| Users | Visiblility | Home | Local | Social | Global |
+| Local users you follow | Publish | ✔ | ✔ | ✔ | ✔ |
+| | Home | ✔ | | ✔ | |
+| | Followers | ✔ | ✔ | ✔ | ✔ |
+| Remote users you follow | Publish | ✔ | | ✔ | ✔ |
+| | Home | ✔ | | ✔ | |
+| | Followers | ✔ | | ✔ | ✔ |
+| Local users you don't follow | Publish | | ✔ | ✔ | ✔ |
+| | Home | | | | |
+| | Followers | | | | |
+| Remote users you don't follow | Publish | | | | ✔ |
+| | Home | | | | |
+| | Followers | | | | |
diff --git a/src/docs/eo-UY/advanced/share-page.md b/src/docs/eo-UY/advanced/share-page.md
index 8c5224f59f..b1959b4d66 100644
--- a/src/docs/eo-UY/advanced/share-page.md
+++ b/src/docs/eo-UY/advanced/share-page.md
@@ -1,19 +1,19 @@
-# シェアページ
+# La paĝo nur por skribi novan noton
`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
-## クエリ文字列一覧
-### 文字
+## La listo de la tekstoj por informpeti
+### Teksto
<dl>
<dt>title</dt>
-<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dd>Tio estas titolo.本文の先頭に[ … ]と挿入されます。</dd>
<dt>text</dt>
-<dd>本文です。</dd>
+<dd>Tio estas teksto</dd>
<dt>url</dt>
-<dd>URLです。末尾に挿入されます。</dd>
+<dd>Tio estas URL.末尾に挿入されます。</dd>
</dl>
-### リプライ情報
+### La informo por respondi
以下のいずれか
<dl>
@@ -23,14 +23,14 @@
<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
</dl>
-### Renote情報
+### La informo por plusendi noton
以下のいずれか
<dl>
<dt>renoteId</dt>
-<dd>Renote先のノートid</dd>
+<dd>la ID de la noto plusendota</dd>
<dt>renoteUri</dt>
-<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+<dd>la URL de la noto plusendota el fora nodo</dd>
</dl>
### Videbleco
@@ -50,5 +50,5 @@
### Dosieroj
<dl>
<dt>fileIds</dt>
-<dd>添付したいファイルのid(カンマ区切りで)</dd>
+<dd>La ID-oj de viaj aldonotaj dosieroj (devas esti apartigita de komoj)</dd>
</dl>
diff --git a/src/docs/pt-PT/features/custom-emoji.md b/src/docs/pt-PT/features/custom-emoji.md
index ed2e92be16..ba9a9743ff 100644
--- a/src/docs/pt-PT/features/custom-emoji.md
+++ b/src/docs/pt-PT/features/custom-emoji.md
@@ -1,2 +1,2 @@
-# カスタム絵文字
+# Emojis Customizados
カスタム絵文字は、インスタンスで用意された画像を絵文字のように使える機能です。 ノート、リアクション、チャット、自己紹介、名前などの場所で使うことができます。 カスタム絵文字をそれらの場所で使うには、絵文字ピッカーボタン(ある場合)を押すか、`:`を入力して絵文字サジェストを表示します。 テキスト内に`:foo:`のような形式の文字列が見つかると、`foo`の部分がカスタム絵文字名と解釈され、表示時には対応したカスタム絵文字に置き換わります。
diff --git a/src/docs/pt-PT/features/theme.md b/src/docs/pt-PT/features/theme.md
index a406f3433c..1da9cbee71 100644
--- a/src/docs/pt-PT/features/theme.md
+++ b/src/docs/pt-PT/features/theme.md
@@ -1,9 +1,9 @@
-# テーマ
+# Temas
-テーマを設定して、Misskeyクライアントの見た目を変更できます。
+É possível mudar a aparência do Misskey com as configurações de tema.
-## テーマの設定
-設定 > テーマ
+## Configurações de tema
+Configurações > Temas.
## テーマを作成する
テーマコードはJSON5で記述されたテーマオブジェクトです。 テーマは以下のようなオブジェクトです。
diff --git a/src/docs/pt-PT/features/widgets.md b/src/docs/pt-PT/features/widgets.md
index a7c2c1d1d6..7c5cacb39a 100644
--- a/src/docs/pt-PT/features/widgets.md
+++ b/src/docs/pt-PT/features/widgets.md
@@ -1,4 +1,4 @@
-# ウィジェット
+# Widgets
ウィジェットは、MisskeyのUI上に設置できる小型の情報表示、操作が行えるパーツです。
ウィジェットを編集するには、ウィジェット編集モードに切り替えます。切り替え方法はUIによって異なります。 ウィジェット編集モードでは、ウィジェットの追加、削除、並び替え、およびそれぞれのウィジェットの設定を行えます。
diff --git a/src/models/entities/note-thread-muting.ts b/src/models/entities/note-thread-muting.ts
new file mode 100644
index 0000000000..b438522a4c
--- /dev/null
+++ b/src/models/entities/note-thread-muting.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'threadId'], { unique: true })
+export class NoteThreadMuting {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column('varchar', {
+ length: 256,
+ })
+ public threadId: string;
+}
diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts
index 9a85532637..4a5411f93d 100644
--- a/src/models/entities/note.ts
+++ b/src/models/entities/note.ts
@@ -47,6 +47,12 @@ export class Note {
@JoinColumn()
public renote: Note | null;
+ @Index()
+ @Column('varchar', {
+ length: 256, nullable: true
+ })
+ public threadId: string | null;
+
@Column('varchar', {
length: 8192, nullable: true
})
diff --git a/src/models/index.ts b/src/models/index.ts
index 4c6f19eaff..7154cca550 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -7,6 +7,7 @@ import { PollVote } from './entities/poll-vote';
import { Meta } from './entities/meta';
import { SwSubscription } from './entities/sw-subscription';
import { NoteWatching } from './entities/note-watching';
+import { NoteThreadMuting } from './entities/note-thread-muting';
import { NoteUnread } from './entities/note-unread';
import { RegistrationTicket } from './entities/registration-tickets';
import { UserRepository } from './repositories/user';
@@ -69,6 +70,7 @@ export const Apps = getCustomRepository(AppRepository);
export const Notes = getCustomRepository(NoteRepository);
export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
export const NoteWatchings = getRepository(NoteWatching);
+export const NoteThreadMutings = getRepository(NoteThreadMuting);
export const NoteReactions = getCustomRepository(NoteReactionRepository);
export const NoteUnreads = getRepository(NoteUnread);
export const Polls = getRepository(Poll);
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 72cefbaac5..9598e87191 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -112,7 +112,7 @@ export class UserRepository extends Repository<User> {
const unread = channels.length > 0 ? await NoteUnreads.findOne({
userId: userId,
- noteChannelId: In(channels.map(x => x.id)),
+ noteChannelId: In(channels.map(x => x.followeeId)),
}) : null;
return unread != null;
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index cf68f3005d..492dc05248 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -288,6 +288,10 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
}
//#endregion
+ if (uri.startsWith(config.url)) {
+ throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
+ }
+
// リモートサーバーからフェッチしてきて登録
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index 84b2f0c51c..eb8c00a10b 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -29,6 +29,7 @@ import { toArray } from '@/prelude/array';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
import { normalizeForSearch } from '@/misc/normalize-for-search';
import { truncate } from '@/misc/truncate';
+import { StatusError } from '@/misc/fetch';
const logger = apLogger;
@@ -116,6 +117,10 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<Use
export async function createPerson(uri: string, resolver?: Resolver): Promise<User> {
if (typeof uri !== 'string') throw new Error('uri is not string');
+ if (uri.startsWith(config.url)) {
+ throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
+ }
+
if (resolver == null) resolver = new Resolver();
const object = await resolver.resolve(uri) as any;
diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts
index 83b27fa866..176a6f7e27 100644
--- a/src/remote/activitypub/renderer/delete.ts
+++ b/src/remote/activitypub/renderer/delete.ts
@@ -4,5 +4,6 @@ import { User } from '@/models/entities/user';
export default (object: any, user: { id: User['id']; host: null }) => ({
type: 'Delete',
actor: `${config.url}/users/${user.id}`,
- object
+ object,
+ published: new Date().toISOString(),
});
diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts
index f9082ffdfc..14115b788d 100644
--- a/src/remote/activitypub/renderer/undo.ts
+++ b/src/remote/activitypub/renderer/undo.ts
@@ -7,6 +7,7 @@ export default (object: any, user: { id: User['id'] }) => {
return {
type: 'Undo',
actor: `${config.url}/users/${user.id}`,
- object
+ object,
+ published: new Date().toISOString(),
};
};
diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts
index d9a8149af3..8bb415d117 100644
--- a/src/remote/activitypub/renderer/update.ts
+++ b/src/remote/activitypub/renderer/update.ts
@@ -7,7 +7,8 @@ export default (object: any, user: { id: User['id'] }) => {
actor: `${config.url}/users/${user.id}`,
type: 'Update',
to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
- object
+ object,
+ published: new Date().toISOString(),
} as any;
return activity;
diff --git a/src/server/api/common/generate-muted-note-thread-query.ts b/src/server/api/common/generate-muted-note-thread-query.ts
new file mode 100644
index 0000000000..7e2cbd498b
--- /dev/null
+++ b/src/server/api/common/generate-muted-note-thread-query.ts
@@ -0,0 +1,17 @@
+import { User } from '@/models/entities/user';
+import { NoteThreadMutings } from '@/models/index';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+export function generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
+ const mutedQuery = NoteThreadMutings.createQueryBuilder('threadMuted')
+ .select('threadMuted.threadId')
+ .where('threadMuted.userId = :userId', { userId: me.id });
+
+ q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
+ q.andWhere(new Brackets(qb => { qb
+ .where(`note.threadId IS NULL`)
+ .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
+ }));
+
+ q.setParameters(mutedQuery.getParameters());
+}
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 74f7911bfe..ffaebd6c95 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -8,6 +8,7 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Brackets } from 'typeorm';
import { generateBlockedUserQuery } from '../../common/generate-block-query';
+import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query';
export const meta = {
tags: ['notes'],
@@ -67,6 +68,7 @@ export default define(meta, async (ps, user) => {
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
+ generateMutedNoteThreadQuery(query, user);
generateBlockedUserQuery(query, user);
if (ps.visibility) {
diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts
index 489902435d..b3913a5e79 100644
--- a/src/server/api/endpoints/notes/state.ts
+++ b/src/server/api/endpoints/notes/state.ts
@@ -1,7 +1,7 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
-import { NoteFavorites, NoteWatchings } from '@/models/index';
+import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index';
export const meta = {
tags: ['notes'],
@@ -25,31 +25,45 @@ export const meta = {
isWatching: {
type: 'boolean' as const,
optional: false as const, nullable: false as const
- }
+ },
+ isMutedThread: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const
+ },
}
}
};
export default define(meta, async (ps, user) => {
- const [favorite, watching] = await Promise.all([
+ const note = await Notes.findOneOrFail(ps.noteId);
+
+ const [favorite, watching, threadMuting] = await Promise.all([
NoteFavorites.count({
where: {
userId: user.id,
- noteId: ps.noteId
+ noteId: note.id,
},
take: 1
}),
NoteWatchings.count({
where: {
userId: user.id,
- noteId: ps.noteId
+ noteId: note.id,
},
take: 1
- })
+ }),
+ NoteThreadMutings.count({
+ where: {
+ userId: user.id,
+ threadId: note.threadId || note.id,
+ },
+ take: 1
+ }),
]);
return {
isFavorited: favorite !== 0,
- isWatching: watching !== 0
+ isWatching: watching !== 0,
+ isMutedThread: threadMuting !== 0,
};
});
diff --git a/src/server/api/endpoints/notes/thread-muting/create.ts b/src/server/api/endpoints/notes/thread-muting/create.ts
new file mode 100644
index 0000000000..2010d54331
--- /dev/null
+++ b/src/server/api/endpoints/notes/thread-muting/create.ts
@@ -0,0 +1,54 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+import { Notes, NoteThreadMutings } from '@/models';
+import { genId } from '@/misc/gen-id';
+import readNote from '@/services/note/read';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true as const,
+
+ kind: 'write:account',
+
+ params: {
+ noteId: {
+ validator: $.type(ID),
+ }
+ },
+
+ errors: {
+ noSuchNote: {
+ message: 'No such note.',
+ code: 'NO_SUCH_NOTE',
+ id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2'
+ }
+ }
+};
+
+export default define(meta, async (ps, user) => {
+ const note = await getNote(ps.noteId).catch(e => {
+ if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw e;
+ });
+
+ const mutedNotes = await Notes.find({
+ where: [{
+ id: note.threadId || note.id,
+ }, {
+ threadId: note.threadId || note.id,
+ }],
+ });
+
+ await readNote(user.id, mutedNotes);
+
+ await NoteThreadMutings.insert({
+ id: genId(),
+ createdAt: new Date(),
+ threadId: note.threadId || note.id,
+ userId: user.id,
+ });
+});
diff --git a/src/server/api/endpoints/notes/thread-muting/delete.ts b/src/server/api/endpoints/notes/thread-muting/delete.ts
new file mode 100644
index 0000000000..05d5691870
--- /dev/null
+++ b/src/server/api/endpoints/notes/thread-muting/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+import { NoteThreadMutings } from '@/models';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true as const,
+
+ kind: 'write:account',
+
+ params: {
+ noteId: {
+ validator: $.type(ID),
+ }
+ },
+
+ errors: {
+ noSuchNote: {
+ message: 'No such note.',
+ code: 'NO_SUCH_NOTE',
+ id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a'
+ }
+ }
+};
+
+export default define(meta, async (ps, user) => {
+ const note = await getNote(ps.noteId).catch(e => {
+ if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw e;
+ });
+
+ await NoteThreadMutings.delete({
+ threadId: note.threadId || note.id,
+ userId: user.id,
+ });
+});
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 8c996bdba6..69d854ab1a 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -10,13 +10,13 @@ import { resolveUser } from '@/remote/resolve-user';
import config from '@/config/index';
import { updateHashtags } from '../update-hashtag';
import { concat } from '@/prelude/array';
-import insertNoteUnread from './unread';
+import { insertNoteUnread } from '@/services/note/unread';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
import { extractMentions } from '@/misc/extract-mentions';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm';
import { extractHashtags } from '@/misc/extract-hashtags';
import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
-import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings } from '@/models/index';
+import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings, NoteThreadMutings } from '@/models/index';
import { DriveFile } from '@/models/entities/drive-file';
import { App } from '@/models/entities/app';
import { Not, getConnection, In } from 'typeorm';
@@ -344,8 +344,15 @@ export default async (user: { id: User['id']; username: User['username']; host:
// 通知
if (data.reply.userHost === null) {
- nm.push(data.reply.userId, 'reply');
- publishMainStream(data.reply.userId, 'reply', noteObj);
+ const threadMuted = await NoteThreadMutings.findOne({
+ userId: data.reply.userId,
+ threadId: data.reply.threadId || data.reply.id,
+ });
+
+ if (!threadMuted) {
+ nm.push(data.reply.userId, 'reply');
+ publishMainStream(data.reply.userId, 'reply', noteObj);
+ }
}
}
@@ -459,6 +466,11 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
replyId: data.reply ? data.reply.id : null,
renoteId: data.renote ? data.renote.id : null,
channelId: data.channel ? data.channel.id : null,
+ threadId: data.reply
+ ? data.reply.threadId
+ ? data.reply.threadId
+ : data.reply.id
+ : null,
name: data.name,
text: data.text,
hasPoll: data.poll != null,
@@ -581,6 +593,15 @@ async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; },
async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) {
+ const threadMuted = await NoteThreadMutings.findOne({
+ userId: u.id,
+ threadId: note.threadId || note.id,
+ });
+
+ if (threadMuted) {
+ continue;
+ }
+
const detailPackedNote = await Notes.pack(note, u, {
detail: true
});
diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts
index 4a9df6083c..29d2b54af8 100644
--- a/src/services/note/unread.ts
+++ b/src/services/note/unread.ts
@@ -1,10 +1,10 @@
import { Note } from '@/models/entities/note';
import { publishMainStream } from '@/services/stream';
import { User } from '@/models/entities/user';
-import { Mutings, NoteUnreads } from '@/models/index';
+import { Mutings, NoteThreadMutings, NoteUnreads } from '@/models/index';
import { genId } from '@/misc/gen-id';
-export default async function(userId: User['id'], note: Note, params: {
+export async function insertNoteUnread(userId: User['id'], note: Note, params: {
// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
isSpecified: boolean;
isMentioned: boolean;
@@ -17,6 +17,13 @@ export default async function(userId: User['id'], note: Note, params: {
if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion
+ // スレッドミュート
+ const threadMute = await NoteThreadMutings.findOne({
+ userId: userId,
+ threadId: note.threadId || note.id,
+ });
+ if (threadMute) return;
+
const unread = {
id: genId(),
noteId: note.id,
diff --git a/test/thread-mute.ts b/test/thread-mute.ts
new file mode 100644
index 0000000000..95601cd903
--- /dev/null
+++ b/test/thread-mute.ts
@@ -0,0 +1,103 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils';
+
+describe('Note thread mute', () => {
+ let p: childProcess.ChildProcess;
+
+ let alice: any;
+ let bob: any;
+ let carol: any;
+
+ before(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ carol = await signup({ username: 'carol' });
+ });
+
+ after(async () => {
+ await shutdownServer(p);
+ });
+
+ it('notes/mentions にミュートしているスレッドの投稿が含まれない', async(async () => {
+ const bobNote = await post(bob, { text: '@alice @carol root note' });
+ const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
+
+ await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+ const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+ const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
+
+ const res = await request('/notes/mentions', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
+ }));
+
+ it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async(async () => {
+ // 状態リセット
+ await request('/i/read-all-unread-notes', {}, alice);
+
+ const bobNote = await post(bob, { text: '@alice @carol root note' });
+
+ await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+ const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+
+ const res = await request('/i', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.body.hasUnreadMentions, false);
+ }));
+
+ it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
+ // 状態リセット
+ await request('/i/read-all-unread-notes', {}, alice);
+
+ const bobNote = await post(bob, { text: '@alice @carol root note' });
+
+ await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+ let fired = false;
+
+ const ws = await connectStream(alice, 'main', async ({ type, body }) => {
+ if (type === 'unreadMention') {
+ if (body === bobNote.id) return;
+ fired = true;
+ }
+ });
+
+ const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+
+ setTimeout(() => {
+ assert.strictEqual(fired, false);
+ ws.close();
+ done();
+ }, 5000);
+ }));
+
+ it('i/notifications にミュートしているスレッドの通知が含まれない', async(async () => {
+ const bobNote = await post(bob, { text: '@alice @carol root note' });
+ const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
+
+ await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+ const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+ const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
+
+ const res = await request('/i/notifications', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false);
+ assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false);
+
+ // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
+ }));
+});
diff --git a/test/utils.ts b/test/utils.ts
index 1a0c54463d..54bcf65ab1 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as WebSocket from 'ws';
+import * as misskey from 'misskey-js';
import fetch from 'node-fetch';
const FormData = require('form-data');
import * as childProcess from 'child_process';
@@ -52,7 +53,7 @@ export const signup = async (params?: any): Promise<any> => {
return res.body;
};
-export const post = async (user: any, params?: any): Promise<any> => {
+export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
const q = Object.assign({
text: 'test'
}, params);