diff --git a/CHANGELOG.md b/CHANGELOG.md index 698b4acead..2196015349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,24 @@ --> +## 2023.10.0 + +### Client +- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正 + +## 2023.9.3 +### General +- Enhance: ノートの翻訳機能の利用可否をロールで設定可能に + +### Client +- Enhance: AiScriptでホストのアドレスを参照する定数`SERVER_URL`を追加 +- Enhance: モデレーションログ機能の強化 +- Enhance: ローカリゼーションの更新 + +### Server +- Fix: Redisに古いバージョンのキャッシュが残っている場合、キャッシュが消えるまでの間通知が届かなくなる問題を修正 +- Fix: 後方互換性の修正 + ## 2023.9.2 ### General diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md index eb9d8108ca..b3ba374b2c 100644 --- a/CHANGELOG_CHERRYPICK.md +++ b/CHANGELOG_CHERRYPICK.md @@ -22,6 +22,34 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE # 릴리즈 노트 이 문서는 CherryPick의 변경 사항만 포함합니다. +## 4.x.x +출시일: unreleased +기반 Misskey 버전: 2023.x.x +Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGELOG.md#2023xx) 문서를 참고하십시오. + +### General +- Feat: 편집한 노트의 기록을 확인할 수 있음 (misskey-dev/misskey#11938) + +### Client +- Feat: 움직이는 이미지를 표시하는 방법을 세분화 + - 마우스를 움직이거나 화면을 터치하고 있으면 이미지를 재생 + - 일정 시간이 경과하면 이미지 재생을 중지 +- Feat: 미디어가 포함된 모든 노트를 접을 수 있음 +- Feat: 클라이언트 업데이트가 있으면 알림 +- Enhance: 유저명, 이름, 인스턴스 이름이 길면 스크롤해서 볼 수 있음 +- Fix: 로그인하지 않은 상태에서 노트 상세 페이지의 노트 작성 폼을 조작할 수 있음 +- Fix: Chromium 기반 브라우저에서 노트 작성 폼의 스크롤 영역이 잘못된 디자인을 표시함 +- Fix: 반응한 사용자 목록의 UI가 드물게 왼쪽 상단에 남아있는 문제 수정 (misskey-dev/misskey#11949) +- Fix: deck ui에서 user list를 볼 때 답글이 표시되지 않음 (misskey-dev/misskey#11951) +- Fix: 노트 상세 페이지의 노트 작성 폼 입력란에 멘션이 기본으로 입력되어 있음 + - 작성란을 눌러야 멘션이 입력되도록 변경 + +### Server +- Feat: 이모티콘 중복 체크 (misskey-dev/misskey#11941) +- Enhance: '내용 숨기기'로 설정된 노트의 주석도 번역에 포함됨 + +--- + ## 4.3.1 출시일: 2023/09/29 기반 Misskey 버전: 2023.9.2 @@ -73,7 +101,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202391](CHANGE - Feat: 알림에서 답글이 달린 노트의 상위 노트를 표시하지 않도록 하는 설정 추가 - Feat: 리노트와 인용 버튼을 표시하는 방법을 선택할 수 있음 - Feat: 알림 위젯에 필터, 모두 읽은 상태로 표시 버튼 추가 -- Feat: 답글에 글 작성란을 표시하는 기능 추가인 +- Feat: 답글에 글 작성란을 표시하는 기능 추가 - Feat: 모바일 환경에서 유저 페이지의 헤더 디자인을 변경할 수 있음 - Spec: 사용자 정의 이모티콘 라이센스를 여러 항목으로 추가할 수 있도록 (MisskeyIO/misskey#130) - Enhance: 새로운 신고가 있는 경우, 네비게이션 바의 제어판 아이콘과 제어판 페이지의 신고 섹션에 점을 표시 diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 788e2c72e4..06b6c21cb8 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1137,6 +1137,9 @@ authenticationRequiredToContinue: "Bitte authentifiziere dich, um fortzufahren" dateAndTime: "Zeit" showRenotes: "Renotes anzeigen" edited: "Bearbeitet" +notificationRecieveConfig: "Benachrichtigungseinstellungen" +mutualFollow: "Gegenseitig gefolgt" +fileAttachedOnly: "Nur Notizen mit Dateien" _announcement: forExistingUsers: "Nur für existierende Nutzer" forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." @@ -2218,3 +2221,6 @@ _moderationLogTypes: unmarkSensitiveDriveFile: "Datei als nicht sensitiv markiert" resolveAbuseReport: "Meldung bearbeitet" createInvitation: "Einladung erstellt" + createAd: "Werbung erstellt" + deleteAd: "Werbung gelöscht" + updateAd: "Werbung aktualisiert" diff --git a/locales/en-US.yml b/locales/en-US.yml index 54fdfcc695..a48f2214d2 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1,5 +1,9 @@ --- _lang_: "English" +youAreRunningBetaClient: "Unreleased version of CherryPick in use!" +cherrypickUpdate: "CherryPick Update" +allMediaNoteCollapse: "Collapse all media notes" +showingAnimatedImagesDescription: "When set to \"Animate on interaction\", the image will play when you hover over it or touch it." showFixedPostFormInReplies: "Show posting form in replies" showFixedPostFormInRepliesDescription: "Only visible in desktop and tablet environments." renoteQuoteButtonSeparation: "Show renote and quote buttons separately" @@ -1200,11 +1204,18 @@ authenticationRequiredToContinue: "Please authenticate to continue" dateAndTime: "Timestamp" showRenotes: "Show renotes" edited: "Edited" +notificationRecieveConfig: "Notification Settings" +mutualFollow: "Mutual follow" +fileAttachedOnly: "Only notes with files" showCatOnly: "Show only cats" additionalPermissionsForFlash: "Allow to add permission to Play" thisFlashRequiresTheFollowingPermissions: "This Play requires the following permissions" doYouWantToAllowThisPlayToAccessYourAccount: "Do you want to allow this Play to access your account?" translateProfile: "Translate profile" +_showingAnimatedImages: + always: "Always animate" + interaction: "Animate on interaction" + inactive: "Stop after a certain amount of time" _messaging: direct: "Direct Message" _tlTutorial: @@ -2136,6 +2147,7 @@ _visibility: disableFederation: "Defederate" disableFederationDescription: "Don't transmit to other instances" _postForm: + signinRequiredPlaceholder: "You must be logged in to create a notes." replyPlaceholder: "Reply to this note..." quotePlaceholder: "Quote this note..." channelPlaceholder: "Post to a channel..." @@ -2389,6 +2401,9 @@ _moderationLogTypes: unmarkSensitiveDriveFile: "File unmarked as sensitive" resolveAbuseReport: "Report resolved" createInvitation: "Invite generated" + createAd: "Ad created" + deleteAd: "Ad deleted" + updateAd: "Ad updated" _abuse: _resolver: 1hour: "one hour" diff --git a/locales/index.d.ts b/locales/index.d.ts index 3c046b3cb3..0cb435484a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -3,6 +3,10 @@ // Do not edit this file directly. export interface Locale { "_lang_": string; + "youAreRunningBetaClient": string; + "cherrypickUpdate": string; + "allMediaNoteCollapse": string; + "showingAnimatedImagesDescription": string; "showFixedPostFormInReplies": string; "showFixedPostFormInRepliesDescription": string; "renoteQuoteButtonSeparation": string; @@ -1211,6 +1215,11 @@ export interface Locale { "thisFlashRequiresTheFollowingPermissions": string; "doYouWantToAllowThisPlayToAccessYourAccount": string; "translateProfile": string; + "_showingAnimatedImages": { + "always": string; + "interaction": string; + "inactive": string; + }; "_messaging": { "direct": string; }; @@ -1730,6 +1739,7 @@ export interface Locale { "descriptionOfRateLimitFactor": string; "canHideAds": string; "canSearchNotes": string; + "canUseTranslator": string; }; "_condition": { "isLocal": string; @@ -2278,6 +2288,7 @@ export interface Locale { "disableFederationDescription": string; }; "_postForm": { + "signinRequiredPlaceholder": string; "replyPlaceholder": string; "quotePlaceholder": string; "channelPlaceholder": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f099ca745d..f475fb51d6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1,5 +1,9 @@ _lang_: "日本語" +youAreRunningBetaClient: "未発売バージョンのCherryPickを利用しています!" +cherrypickUpdate: "CherryPickアップデート" +allMediaNoteCollapse: "すべてのメディアノートを省略して表示" +showingAnimatedImagesDescription: "「インタラクト時に再生」に設定すると、画像の上にマウスを置いたり、画像をタッチすると再生されます。" showFixedPostFormInReplies: "返信に投稿フォームを表示する" showFixedPostFormInRepliesDescription: "デスクトップとタブレット環境でのみ表示されます。" renoteQuoteButtonSeparation: "リノートと引用ボタンを分けて表示する" @@ -1209,6 +1213,11 @@ thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求 doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか?" translateProfile: "プロフィールを翻訳する" +_showingAnimatedImages: + always: "常に再生" + interaction: "インタラクト時に再生" + inactive: "一定時間経過すると再生" + _messaging: direct: "ダイレクトメッセージ" @@ -1649,7 +1658,8 @@ _role: rateLimitFactor: "レートリミット" descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" canHideAds: "広告の非表示" - canSearchNotes: "ノート検索の利用可否" + canSearchNotes: "ノート検索の利用" + canUseTranslator: "翻訳機能の利用" _condition: isLocal: "ローカルユーザー" isRemote: "リモートユーザー" @@ -2191,6 +2201,7 @@ _visibility: disableFederationDescription: "他サーバーへの配信を行いません" _postForm: + signinRequiredPlaceholder: "ノートを作成するにはログインが必要です。" replyPlaceholder: "このノートに返信..." quotePlaceholder: "このノートを引用..." channelPlaceholder: "チャンネルに投稿..." diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 511c129ca0..6dfe2fbb57 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1,5 +1,9 @@ --- _lang_: "한국어" +youAreRunningBetaClient: "아직 출시되지 않은 버전의 CherryPick를 이용하고 있어요!" +cherrypickUpdate: "CherryPick 업데이트" +allMediaNoteCollapse: "모든 미디어 노트 간략화하기" +showingAnimatedImagesDescription: "'건드리면 움직임'으로 설정하면 이미지 위에 마우스를 올리거나 이미지를 터치하면 움직여요." showFixedPostFormInReplies: "답글에 글 작성란 표시" showFixedPostFormInRepliesDescription: "데스크톱과 태블릿 환경에서만 표시돼요." renoteQuoteButtonSeparation: "리노트와 인용 버튼을 분리해서 표시하기" @@ -463,6 +467,9 @@ totp: "인증 앱" totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력" moderator: "모더레이터" moderation: "모더레이션" +moderationNote: "모더레이션 노트" +addModerationNote: "모더레이션 노트 추가하기" +moderationLogs: "모더레이션 로그" nUsersMentioned: "{n}명이 언급함" securityKeyAndPasskey: "보안 키 또는 패스 키" securityKey: "보안 키" @@ -1186,11 +1193,28 @@ youHaveUnreadAnnouncements: "읽지 않은 공지사항이 있어요." useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주세요." replies: "답글" renotes: "리노트" +loadReplies: "답글 보기" +loadConversation: "대화 보기" +pinnedList: "고정해놓은 리스트" +keepScreenOn: "기기 화면 항상 켜기" +verifiedLink: "이 링크의 소유자임을 확인했어요." +notifyNotes: "새 노트 알림 켜기" +unnotifyNotes: "새 노트 알림 끄기" +authentication: "인증" +showRenotes: "리노트 표시" +edited: "수정됨" +notificationRecieveConfig: "알림 설정" +mutualFollow: "맞팔로우" +fileAttachedOnly: "파일이 포함된 노트만" showCatOnly: "고양이만 보기" additionalPermissionsForFlash: "Play에 대한 추가 권한" thisFlashRequiresTheFollowingPermissions: "이 Play는 다음 권한을 요구해요" doYouWantToAllowThisPlayToAccessYourAccount: "이 Play가 계정에 접근하도록 허용할까요?" translateProfile: "프로필 번역하기" +_showingAnimatedImages: + always: "항상 움직임" + interaction: "건드리면 움직임" + inactive: "일정 시간이 지나면 멈춤" _messaging: direct: "다이렉트 메시지" _tlTutorial: @@ -1297,6 +1321,12 @@ _event: performers: "출연자" _serverSettings: iconUrl: "아이콘 URL" + appIconUsageExample: "예를 들어, PWA나 스마트폰 홈 화면에 북마크로 추가되었을 때 등" + appIconStyleRecommendation: "아이콘이 원형 또는 둥근 사각형으로 잘리는 경우가 있으므로, 가장자리 여백이 충분한 사진을 사용하는 것을 추천해요." + appIconResolutionMustBe: "해상도는 반드시 {resolution} 이어야 해요." + manifestJsonOverride: "manifest.json 덮어쓰기" + shortName: "약칭" + shortNameDescription: "서버의 정식 명칭이 긴 경우, 대신 표시할 수 있는 약칭이나 통칭." _accountMigration: moveFrom: "다른 계정에서 이 계정으로 이사" moveFromSub: "다른 계정에 대한 별칭을 생성" @@ -2110,6 +2140,7 @@ _visibility: disableFederation: "연합에 보내지 않기" disableFederationDescription: "다른 서버로 보내지 않을래요" _postForm: + signinRequiredPlaceholder: "노트를 작성하려면 로그인이 필요해요." replyPlaceholder: "이 노트에 답글..." quotePlaceholder: "이 노트를 인용..." channelPlaceholder: "채널에 게시하기..." diff --git a/locales/th-TH.yml b/locales/th-TH.yml index a1e63328bc..b602e33c67 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1134,6 +1134,9 @@ authentication: "การตรวจสอบสิทธิ์" dateAndTime: "เวลาประทับ" showRenotes: "แสดงรีโน้ต" edited: "แก้ไขแล้ว" +notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน" +mutualFollow: "ติดตามซึ่งกันและกัน" +fileAttachedOnly: "เฉพาะโน้ตที่มีไฟล์เท่านั้น" _announcement: forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น" forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน" @@ -2190,3 +2193,6 @@ _moderationLogTypes: resetPassword: "รีเซ็ตรหัสผ่าน" resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว" createInvitation: "สร้างคำเชิญ" + createAd: "สร้างโฆษณาแล้ว" + deleteAd: "ลบโฆษณาออกแล้ว" + updateAd: "อัปเดตโฆษณาแล้ว" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 02fa02e5b2..49e4c729a4 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1137,6 +1137,9 @@ authenticationRequiredToContinue: "要继续,请先进行验证" dateAndTime: "日期和时间" showRenotes: "显示转帖" edited: "已编辑" +notificationRecieveConfig: "通知接收设置" +mutualFollow: "互相关注" +fileAttachedOnly: "仅限媒体" _announcement: forExistingUsers: "仅限现有用户" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" @@ -2216,3 +2219,6 @@ _moderationLogTypes: unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体" resolveAbuseReport: "处理举报" createInvitation: "发行邀请码" + createAd: "创建了广告" + deleteAd: "删除了广告" + updateAd: "更新了广告" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 1586e1c0f2..d9ef28186e 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1136,6 +1136,8 @@ authentication: "驗證" authenticationRequiredToContinue: "請於繼續前完成驗證" dateAndTime: "日期與時間" showRenotes: "顯示轉發貼文" +edited: "已編輯" +mutualFollow: "互相追隨" _announcement: forExistingUsers: "僅限既有的使用者" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" @@ -2217,3 +2219,6 @@ _moderationLogTypes: unmarkSensitiveDriveFile: "撤銷標記為敏感檔案" resolveAbuseReport: "解決檢舉" createInvitation: "建立邀請碼" + createAd: "建立廣告" + deleteAd: "刪除廣告" + updateAd: "更新廣告" diff --git a/package.json b/package.json index 85c8745f6a..2dff572c24 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cherrypick", "version": "4.3.1", - "basedMisskeyVersion": "2023.9.2", + "basedMisskeyVersion": "2023.9.3", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1696044626209-noteEditHistory.js b/packages/backend/migration/1696044626209-noteEditHistory.js new file mode 100644 index 0000000000..acfc4608a2 --- /dev/null +++ b/packages/backend/migration/1696044626209-noteEditHistory.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class NoteEditHistory1696044626209 { + name = 'NoteEditHistory1696044626209' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "noteEditHistory" varchar(3000) array DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP "noteEditHistory"`); + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index c9d9de4335..9630599438 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -382,6 +382,18 @@ export class CustomEmojiService implements OnApplicationShutdown { this.cache.set(`${emoji.name} ${emoji.host}`, emoji); } } + /** + * ローカル内の絵文字に重複がないかチェックします + * @param name 絵文字名 + */ + public async isDuplicateCheck(name: string): Promise { + return (await this.emojisRepository.findOneBy({ name, host: IsNull() })) !== null; + } + + @bindThis + public async getEmojiById(id: string): Promise { + return this.emojisRepository.findOneBy({ id }); + } @bindThis public dispose(): void { diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 835cc1321e..248ab0f1d8 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -78,7 +78,7 @@ export class EmailService { a { text-decoration: none; - color: #86b300; + color: rgb(255, 188, 220); } a:hover { text-decoration: underline; @@ -92,7 +92,7 @@ export class EmailService { } main > header { padding: 32px; - background: #86b300; + background: rgb(255, 188, 220); } main > header > img { max-width: 128px; diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 56ec025571..bda490883e 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -80,7 +80,10 @@ export class NotificationService implements OnApplicationShutdown { notifierId?: MiUser['id'] | null, ): Promise { const profile = await this.cacheService.userProfileCache.fetch(notifieeId); - const recieveConfig = profile.notificationRecieveConfig[type]; + + // 古いMisskeyバージョンのキャッシュが残っている可能性がある + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const recieveConfig = (profile.notificationRecieveConfig ?? {})[type]; if (recieveConfig?.type === 'never') { return null; } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 99dbcabb2a..5b601d4bf9 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -34,6 +34,7 @@ export type RolePolicies = { inviteExpirationTime: number; canManageCustomEmojis: boolean; canSearchNotes: boolean; + canUseTranslator: boolean; canHideAds: boolean; driveCapacityMb: number; alwaysMarkNsfw: boolean; @@ -60,6 +61,7 @@ export const DEFAULT_POLICIES: RolePolicies = { inviteExpirationTime: 0, canManageCustomEmojis: false, canSearchNotes: false, + canUseTranslator: true, canHideAds: false, driveCapacityMb: 100, alwaysMarkNsfw: false, @@ -306,6 +308,7 @@ export class RoleService implements OnApplicationShutdown { inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), + canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 48c81277f1..e89a4c4dba 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -323,6 +323,7 @@ export class NoteEntityService implements OnModuleInit { id: note.id, createdAt: note.createdAt.toISOString(), updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined, + noteEditHistory: note.noteEditHistory.length ? note.noteEditHistory : undefined, userId: note.userId, user: this.userEntityService.pack(note.user ?? note.userId, me, { detail: false, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index d91c208118..3a0d87bc56 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -490,6 +490,7 @@ export class UserEntityService implements OnModuleInit { hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), mutedWords: profile!.mutedWords, mutedInstances: profile!.mutedInstances, + mutingNotificationTypes: [], // 後方互換性のため notificationRecieveConfig: profile!.notificationRecieveConfig, emailNotificationTypes: profile!.emailNotificationTypes, achievements: profile!.achievements, diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index 11a363522a..ca8a290392 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -29,6 +29,13 @@ export class MiNote { }) public updatedAt: Date | null; + @Column('varchar', { + length: 3000, + array: true, + default: '{}', + }) + public noteEditHistory: string[]; + @Index() @Column({ ...id(), diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 3686d80a10..9b214295c0 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -22,6 +22,10 @@ export const packedNoteSchema = { optional: true, nullable: true, format: 'date-time', }, + noteEditHistory: { + type: 'array', + optional: true, nullable: false, + }, deletedAt: { type: 'string', optional: true, nullable: true, diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index e5e69606b7..b54bde9305 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -119,7 +119,7 @@ export class NodeinfoServerService { enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, proxyAccountName: proxyAccount ? proxyAccount.username : null, - themeColor: meta.themeColor ?? '#86b300', + themeColor: meta.themeColor ?? 'rgb(255, 188, 220)', }, }; if (version >= 21) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 9a71afc93f..98c26af24d 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -23,6 +23,11 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', }, + duplicateName: { + message: 'Duplicate name.', + code: 'DUPLICATE_NAME', + id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', + }, }, } as const; @@ -64,7 +69,8 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); - + const duplicateCheck = await this.customEmojiService.isDuplicateCheck(ps.name); + if (duplicateCheck != null) throw new ApiError(meta.errors.duplicateName); const emoji = await this.customEmojiService.add({ driveFile, name: ps.name, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 8738c428ab..4da836092e 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -74,6 +74,15 @@ export default class extends Endpoint { // eslint- driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } + const oldemoji = await this.customEmojiService.getEmojiById(ps.id); + if (oldemoji !== null) { + if (ps.name !== oldemoji.name) { + const duplicateCheck = await this.customEmojiService.isDuplicateCheck(ps.name); + if (duplicateCheck) throw new ApiError(meta.errors.sameNameEmojiExists); + } + } else { + throw new ApiError(meta.errors.noSuchEmoji); + } await this.customEmojiService.update(ps.id, { driveFile, diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 3a894fcad4..44885bad07 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -14,12 +14,13 @@ import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { createTemp } from '@/misc/create-temp.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], - requireCredential: false, + requireCredential: true, res: { type: 'object', @@ -27,6 +28,11 @@ export const meta = { }, errors: { + unavailable: { + message: 'Translate of notes unavailable.', + code: 'UNAVAILABLE', + id: '50a70314-2d8a-431b-b433-efa5cc56444c', + }, noSuchNote: { message: 'No such note.', code: 'NO_SUCH_NOTE', @@ -56,14 +62,20 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, private metaService: MetaService, private httpRequestService: HttpRequestService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { + const policies = await this.roleService.getUserPolicies(me.id); + if (!policies.canUseTranslator) { + throw new ApiError(meta.errors.unavailable); + } + const note = await this.getterService.getNote(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - if (!(await this.noteEntityService.isVisibleForMe(note, me ? me.id : null))) { + if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) { return 204; // TODO: 良い感じのエラー返す } @@ -91,12 +103,12 @@ export default class extends Endpoint { // eslint- if (instance.deeplAuthKey == null) { return 204; // TODO: 良い感じのエラー返す } - translationResult = await this.translateDeepL(note.text, targetLang, instance.deeplAuthKey, instance.deeplIsPro, instance.translatorType); + translationResult = await this.translateDeepL((note.cw ? note.cw + '\n' : '') + note.text, targetLang, instance.deeplAuthKey, instance.deeplIsPro, instance.translatorType); } else if (instance.translatorType === 'google_no_api') { let targetLang = ps.targetLang; if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; - const { text, raw } = await translate(note.text, { to: targetLang }); + const { text, raw } = await translate((note.cw ? note.cw + '\n' : '') + note.text, { to: targetLang }); return { sourceLang: raw.src, @@ -107,7 +119,7 @@ export default class extends Endpoint { // eslint- if (instance.ctav3SaKey == null) { return 204; } else if (instance.ctav3ProjectId == null) { return 204; } else if (instance.ctav3Location == null) { return 204; } translationResult = await this.apiCloudTranslationAdvanced( - note.text, targetLang, instance.ctav3SaKey, instance.ctav3ProjectId, instance.ctav3Location, instance.ctav3Model, instance.ctav3Glossary, instance.translatorType, + (note.cw ? note.cw + '\n' : '') + note.text, targetLang, instance.ctav3SaKey, instance.ctav3ProjectId, instance.ctav3Location, instance.ctav3Model, instance.ctav3Glossary, instance.translatorType, ); } else { throw new Error('Unsupported translator type'); diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts index c6f5a446fd..c407a7045a 100644 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -73,11 +73,11 @@ export default class extends Endpoint { // eslint- if (note.userId !== me.id) { throw new ApiError(meta.errors.noSuchNote); } - await this.notesRepository.update({ id: note.id }, { updatedAt: new Date(), cw: ps.cw, text: ps.text, + noteEditHistory: [...note.noteEditHistory, note.text!], }); this.globalEventService.publishNoteStream(note.id, 'updated', { diff --git a/packages/backend/src/server/api/endpoints/users/translate.ts b/packages/backend/src/server/api/endpoints/users/translate.ts index 5884023e27..5440b6a561 100644 --- a/packages/backend/src/server/api/endpoints/users/translate.ts +++ b/packages/backend/src/server/api/endpoints/users/translate.ts @@ -13,12 +13,13 @@ import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { createTemp } from '@/misc/create-temp.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['users'], - requireCredential: false, + requireCredential: true, res: { type: 'object', @@ -26,6 +27,11 @@ export const meta = { }, errors: { + unavailable: { + message: 'Translate of notes unavailable.', + code: 'UNAVAILABLE', + id: '50a70314-2d8a-431b-b433-efa5cc56444c', + }, noSuchDescription: { message: 'No such description.', code: 'NO_SUCH_DESCRIPTION', @@ -54,8 +60,14 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, private metaService: MetaService, private httpRequestService: HttpRequestService, + private roleService: RoleService, ) { - super(meta, paramDef, async (ps) => { + super(meta, paramDef, async (ps, me) => { + const policies = await this.roleService.getUserPolicies(me.id); + if (!policies.canUseTranslator) { + throw new ApiError(meta.errors.unavailable); + } + const target = await this.getterService.getUserProfiles(ps.userId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchDescription); throw err; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 4cc6a72709..6bbf2c48fc 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -188,7 +188,7 @@ export class ClientServerService { // Authenticate fastify.addHook('onRequest', async (request, reply) => { // %71ueueとかでリクエストされたら困るため - const url = decodeURI(request.routerPath); + const url = decodeURI(request.routeOptions.url); if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) { const token = request.cookies.token; if (token == null) { diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index d1229f8145..7653262dd4 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -23,8 +23,8 @@ html meta(charset='utf-8') meta(name='application-name' content='CherryPick') meta(name='referrer' content='origin') - meta(name='theme-color' content= themeColor || '#86b300') - meta(name='theme-color-orig' content= themeColor || '#86b300') + meta(name='theme-color' content= themeColor || 'rgb(255, 188, 220)') + meta(name='theme-color-orig' content= themeColor || 'rgb(255, 188, 220)') meta(property='og:site_name' content= instanceName || 'CherryPick') meta(name='viewport' content='width=device-width, initial-scale=1') link(rel='icon' href= icon || '/favicon.ico') diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 80b771f7bf..d51eddd854 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -167,6 +167,7 @@ describe('ユーザー', () => { unreadAnnouncements: user.unreadAnnouncements, mutedWords: user.mutedWords, mutedInstances: user.mutedInstances, + mutingNotificationTypes: user.mutingNotificationTypes, notificationRecieveConfig: user.notificationRecieveConfig, emailNotificationTypes: user.emailNotificationTypes, achievements: user.achievements, @@ -416,6 +417,7 @@ describe('ユーザー', () => { assert.deepStrictEqual(response.unreadAnnouncements, []); assert.deepStrictEqual(response.mutedWords, []); assert.deepStrictEqual(response.mutedInstances, []); + assert.deepStrictEqual(response.mutingNotificationTypes, []); assert.deepStrictEqual(response.notificationRecieveConfig, {}); assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest', 'groupInvited']); assert.deepStrictEqual(response.achievements, []); diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index b360eb8624..e6cbcf9f10 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -2701,6 +2701,7 @@ type Note = { id: ID; createdAt: DateString; updatedAt?: DateString | null; + noteEditHistory: string[]; text: string | null; cw: string | null; user: User; @@ -3046,7 +3047,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:659:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts -// src/entities.ts:602:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:603:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/cherrypick-js/src/entities.ts b/packages/cherrypick-js/src/entities.ts index e1be77c5dc..29fd12d4a9 100644 --- a/packages/cherrypick-js/src/entities.ts +++ b/packages/cherrypick-js/src/entities.ts @@ -178,6 +178,7 @@ export type Note = { id: ID; createdAt: DateString; updatedAt?: DateString | null; + noteEditHistory: string[]; text: string | null; cw: string | null; user: User; diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index be164bb2e8..ccc01620e4 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -92,7 +92,7 @@ async function renderChart() { await nextTick(); - const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; + const color = defaultStore.state.darkMode ? 'rgb(255, 207, 230)' : 'rgb(255, 188, 220)'; // 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index bbde1fc7fe..2ec7a04173 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -77,6 +77,19 @@ $height: 2ex; font-size: 0.9em; font-weight: bold; white-space: nowrap; - overflow: visible; + overflow: scroll; + overflow-wrap: anywhere; + max-width: 300px; + text-overflow: ellipsis; + + &::-webkit-scrollbar { + display: none; + } +} + +@container (max-width: 500px) { + .name { + max-width: 100px; + } } diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 68ee9f8d88..6bfe725c73 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -27,6 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only :width="image.properties.width" :height="image.properties.height" :style="hide ? 'filter: brightness(0.7);' : null" + @mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''" + @mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''" + @touchstart="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''" + @touchend="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''" /> @@ -51,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkPostFormSimple.vue b/packages/frontend/src/components/MkPostFormSimple.vue index 5d06b0afc1..57b72908b6 100644 --- a/packages/frontend/src/components/MkPostFormSimple.vue +++ b/packages/frontend/src/components/MkPostFormSimple.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only :enterFromClass="defaultStore.state.animation ? $style.transition_header_enterFrom : ''" :leaveToClass="defaultStore.state.animation ? $style.transition_header_leaveTo : ''" > - + @@ -72,16 +72,16 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.notSpecifiedMentionWarning }} - {{ i18n.ts.add }} - - - + + + {{ maxTextLength - textLength }} - + {{ submitText }} - + @@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only :enterFromClass="defaultStore.state.animation ? $style.transition_footer_enterFrom : ''" :leaveToClass="defaultStore.state.animation ? $style.transition_footer_leaveTo : ''" > - - @@ -37,10 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only - - {{ i18n.ts.showMore }} + + + {{ i18n.ts.showMore }} + ({{ collapseLabel }}) + - + {{ i18n.ts.showLess }} @@ -104,7 +106,6 @@ import * as Misskey from 'cherrypick-js'; import * as os from '@/os.js'; import MkMediaList from '@/components/MkMediaList.vue'; import MkPoll from '@/components/MkPoll.vue'; -import MkDetailsButton from '@/components/MkDetailsButton.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -124,6 +125,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { MenuItem } from '@/types/menu.js'; +import { concat } from '@/scripts/array.js'; const el = shallowRef(); const menuButton = shallowRef(); @@ -135,10 +137,16 @@ const canRenote = computed(() => ['public', 'home'].includes(props.note.visibili const isDeleted = ref(false); const currentClip = inject | null>('currentClip', null); -const showContent = ref(false); +const showContent = ref(true); const translation = ref(null); const translating = ref(false); +const collapseLabel = computed(() => { + return concat([ + props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length })] : [], + ] as string[][]).join(' / '); +}); + const props = defineProps<{ note: Misskey.entities.Note; showSubNoteFooterButton: boolean; @@ -149,7 +157,7 @@ let note = $ref(deepClone(props.note)); const isLong = shouldCollapsed(props.note); const isMFM = shouldMfmCollapsed(props.note); -const collapsed = $ref(isLong || (isMFM && defaultStore.state.collapseDefault)); +const collapsed = $ref(isLong || (isMFM && defaultStore.state.collapseDefault) || defaultStore.state.allMediaNoteCollapse || note.files.length > 0 || note.poll); useNoteCapture({ rootEl: el, diff --git a/packages/frontend/src/components/MkUserSetupDialog.MisskeyFlavoredMarkdown.vue b/packages/frontend/src/components/MkUserSetupDialog.MisskeyFlavoredMarkdown.vue index fd9b976629..5b7cb7057c 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.MisskeyFlavoredMarkdown.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.MisskeyFlavoredMarkdown.vue @@ -40,6 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.photosensitiveSeizuresWarning }} {{ i18n.ts.disableShowingAnimatedImages }}{{ i18n.ts.disableShowingAnimatedImagesDescription }} + + {{ i18n.ts._showingAnimatedImages.always }} + {{ i18n.ts._showingAnimatedImages.interaction }} + {{ i18n.ts._showingAnimatedImages.inactive }} + {{ i18n.ts.showingAnimatedImagesDescription }} + {{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }} @@ -52,12 +58,14 @@ import { i18n } from '@/i18n.js'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; +import MkRadios from '@/components/MkRadios.vue'; import { defaultStore } from '@/store.js'; const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm')); const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); +const showingAnimatedImages = computed(defaultStore.makeGetterSetter('showingAnimatedImages'));