mirror of
https://github.com/kokonect-link/cherrypick
synced 2024-12-17 16:18:27 +09:00
Merge branch 'develop' of https://github.com/kokonect-link/cherrypick
This commit is contained in:
commit
7e11a01c04
18
CHANGELOG.md
18
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
|
||||
|
@ -22,8 +22,36 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE
|
||||
# 릴리즈 노트
|
||||
이 문서는 CherryPick의 변경 사항만 포함합니다.
|
||||
|
||||
## 4.3.2
|
||||
출시일: 2023/10/3<br>
|
||||
기반 Misskey 버전: 2023.9.3<br>
|
||||
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<br>
|
||||
출시일: 2023/9/29<br>
|
||||
기반 Misskey 버전: 2023.9.2<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202392](CHANGELOG.md#202392) 문서를 참고하십시오.
|
||||
|
||||
@ -34,7 +62,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202392](CHANGE
|
||||
---
|
||||
|
||||
## 4.3.0
|
||||
출시일: 2023/09/29<br>
|
||||
출시일: 2023/9/29<br>
|
||||
기반 Misskey 버전: 2023.9.1<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202391](CHANGELOG.md#202391) 문서를 참고하십시오.
|
||||
|
||||
@ -73,7 +101,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202391](CHANGE
|
||||
- Feat: 알림에서 답글이 달린 노트의 상위 노트를 표시하지 않도록 하는 설정 추가
|
||||
- Feat: 리노트와 인용 버튼을 표시하는 방법을 선택할 수 있음
|
||||
- Feat: 알림 위젯에 필터, 모두 읽은 상태로 표시 버튼 추가
|
||||
- Feat: 답글에 글 작성란을 표시하는 기능 추가인
|
||||
- Feat: 답글에 글 작성란을 표시하는 기능 추가
|
||||
- Feat: 모바일 환경에서 유저 페이지의 헤더 디자인을 변경할 수 있음
|
||||
- Spec: 사용자 정의 이모티콘 라이센스를 여러 항목으로 추가할 수 있도록 (MisskeyIO/misskey#130)
|
||||
- Enhance: 새로운 신고가 있는 경우, 네비게이션 바의 제어판 아이콘과 제어판 페이지의 신고 섹션에 점을 표시
|
||||
@ -124,7 +152,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#202391](CHANGE
|
||||
---
|
||||
|
||||
## 13.14.2-cp-4.2.0
|
||||
출시일: 2023/07/29<br>
|
||||
출시일: 2023/7/29<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#13142](CHANGELOG.md#13142) 문서를 참고하십시오.
|
||||
|
||||
### General
|
||||
@ -179,7 +207,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#13142](CHANGEL
|
||||
---
|
||||
|
||||
## 13.13.2-cp-4.1.0
|
||||
출시일: 2023/06/20<br>
|
||||
출시일: 2023/6/20<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#13132](CHANGELOG.md#13132) 문서를 참고하십시오.
|
||||
|
||||
### General
|
||||
@ -262,7 +290,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#13132](CHANGEL
|
||||
---
|
||||
|
||||
## 13.13.1-cp-4.0.0
|
||||
출시일: 2023/06/06<br>
|
||||
출시일: 2023/6/6<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#13131](CHANGELOG.md#13131) 문서를 참고하십시오.
|
||||
|
||||
### General
|
||||
@ -323,7 +351,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#13131](CHANGEL
|
||||
---
|
||||
|
||||
## 13.5.6-cp-3.1.0
|
||||
출시일: 2023/02/10<br>
|
||||
출시일: 2023/2/10<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md](CHANGELOG.md) 문서를 참고하십시오.
|
||||
|
||||
## NOTE
|
||||
@ -332,7 +360,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md](CHANGELOG.md)
|
||||
---
|
||||
|
||||
## 13.5.5-cp-3.1.0
|
||||
출시일: 2023/02/10<br>
|
||||
출시일: 2023/2/10<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md](CHANGELOG.md) 문서를 참고하십시오.
|
||||
|
||||
## NOTE
|
||||
@ -362,7 +390,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md](CHANGELOG.md)
|
||||
---
|
||||
|
||||
## 12.119.0-cp-3.0.0
|
||||
출시일: 2022/09/16<br>
|
||||
출시일: 2022/9/16<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md](CHANGELOG.md) 문서를 참고하십시오.
|
||||
|
||||
## NOTE
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
11
locales/index.d.ts
vendored
11
locales/index.d.ts
vendored
@ -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;
|
||||
|
@ -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: "チャンネルに投稿..."
|
||||
|
@ -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: "채널에 게시하기..."
|
||||
|
@ -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: "อัปเดตโฆษณาแล้ว"
|
||||
|
@ -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: "更新了广告"
|
||||
|
@ -1136,6 +1136,8 @@ authentication: "驗證"
|
||||
authenticationRequiredToContinue: "請於繼續前完成驗證"
|
||||
dateAndTime: "日期與時間"
|
||||
showRenotes: "顯示轉發貼文"
|
||||
edited: "已編輯"
|
||||
mutualFollow: "互相追隨"
|
||||
_announcement:
|
||||
forExistingUsers: "僅限既有的使用者"
|
||||
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
|
||||
@ -2217,3 +2219,6 @@ _moderationLogTypes:
|
||||
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
|
||||
resolveAbuseReport: "解決檢舉"
|
||||
createInvitation: "建立邀請碼"
|
||||
createAd: "建立廣告"
|
||||
deleteAd: "刪除廣告"
|
||||
updateAd: "更新廣告"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cherrypick",
|
||||
"version": "4.3.1",
|
||||
"basedMisskeyVersion": "2023.9.2",
|
||||
"version": "4.3.2",
|
||||
"basedMisskeyVersion": "2023.9.3",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
16
packages/backend/migration/1696044626209-noteEditHistory.js
Normal file
16
packages/backend/migration/1696044626209-noteEditHistory.js
Normal file
@ -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"`);
|
||||
}
|
||||
}
|
@ -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<boolean> {
|
||||
return (await this.emojisRepository.findOneBy({ name, host: IsNull() })) !== null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getEmojiById(id: string): Promise<MiEmoji | null> {
|
||||
return this.emojisRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
|
@ -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;
|
||||
|
@ -80,7 +80,10 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
notifierId?: MiUser['id'] | null,
|
||||
): Promise<MiNotification | null> {
|
||||
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;
|
||||
}
|
||||
|
@ -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)),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -29,6 +29,13 @@ export class MiNote {
|
||||
})
|
||||
public updatedAt: Date | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 3000,
|
||||
array: true,
|
||||
default: '{}',
|
||||
})
|
||||
public noteEditHistory: string[];
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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,
|
||||
|
@ -74,6 +74,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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,
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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');
|
||||
|
@ -73,11 +73,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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', {
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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;
|
||||
|
@ -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) {
|
||||
|
@ -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')
|
||||
|
@ -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, []);
|
||||
|
@ -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)
|
||||
|
@ -178,6 +178,7 @@ export type Note = {
|
||||
id: ID;
|
||||
createdAt: DateString;
|
||||
updatedAt?: DateString | null;
|
||||
noteEditHistory: string[];
|
||||
text: string | null;
|
||||
cw: string | null;
|
||||
user: User;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -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 : ''"
|
||||
/>
|
||||
</component>
|
||||
<template v-if="hide">
|
||||
@ -51,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import { onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
@ -76,9 +80,12 @@ const props = withDefaults(defineProps<{
|
||||
let hide = $ref(true);
|
||||
let darkMode: boolean = $ref(defaultStore.state.darkMode);
|
||||
|
||||
let playAnimation = $ref(true);
|
||||
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
|
||||
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
|
||||
? props.image.url
|
||||
: defaultStore.state.disableShowingAnimatedImages
|
||||
: defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
|
||||
? getStaticImageUrl(props.image.url)
|
||||
: props.image.thumbnailUrl,
|
||||
);
|
||||
@ -92,6 +99,12 @@ function onclick() {
|
||||
}
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
playAnimation = true;
|
||||
clearTimeout(playAnimationTimer);
|
||||
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
}
|
||||
|
||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||
watch(() => props.image, () => {
|
||||
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
|
||||
@ -117,6 +130,21 @@ function showMenu(ev: MouseEvent) {
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.addEventListener('mousemove', resetTimer);
|
||||
window.addEventListener('touchstart', resetTimer);
|
||||
window.addEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.removeEventListener('mousemove', resetTimer);
|
||||
window.removeEventListener('touchstart', resetTimer);
|
||||
window.removeEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@ -126,7 +154,7 @@ function showMenu(ev: MouseEvent) {
|
||||
|
||||
.sensitive {
|
||||
position: relative;
|
||||
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
@ -99,10 +99,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
<button v-if="(isLong || (isMFM && defaultStore.state.collapseDefault)) && collapsed" v-vibrate="5" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
||||
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
|
||||
<button v-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || (appearNote.files.length > 0 && defaultStore.state.allMediaNoteCollapse)) && collapsed" v-vibrate="5" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
||||
<span :class="$style.collapsedLabel">
|
||||
{{ i18n.ts.showMore }}
|
||||
<span v-if="appearNote.files.length > 0" :class="$style.label">({{ collapseLabel }})</span>
|
||||
</span>
|
||||
</button>
|
||||
<button v-else-if="(isLong || (isMFM && defaultStore.state.collapseDefault)) && !collapsed" v-vibrate="5" :class="$style.showLess" class="_button" @click="collapsed = true">
|
||||
<button v-else-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || defaultStore.state.allMediaNoteCollapse) && !collapsed" v-vibrate="5" :class="$style.showLess" class="_button" @click="collapsed = true">
|
||||
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -185,7 +188,6 @@ import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import MkEvent from '@/components/MkEvent.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
@ -212,6 +214,7 @@ import { mainRouter } from '@/router.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { concat } from '@/scripts/array.js';
|
||||
|
||||
let showEl = $ref(false);
|
||||
|
||||
@ -259,7 +262,7 @@ const showContent = ref(false);
|
||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||
const isLong = shouldCollapsed(appearNote);
|
||||
const isMFM = shouldMfmCollapsed(appearNote);
|
||||
const collapsed = ref(appearNote.cw == null && (isLong || (isMFM && defaultStore.state.collapseDefault)));
|
||||
const collapsed = ref(appearNote.cw == null && (isLong || (isMFM && defaultStore.state.collapseDefault) || defaultStore.state.allMediaNoteCollapse));
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||
const translation = ref<any>(null);
|
||||
@ -267,6 +270,12 @@ const translating = ref(false);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||
|
||||
const collapseLabel = computed(() => {
|
||||
return concat([
|
||||
appearNote.files && appearNote.files.length !== 0 ? [i18n.t('_cw.files', { count: appearNote.files.length })] : [],
|
||||
] as string[][]).join(' / ');
|
||||
});
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
'e|a|plus': () => react(true),
|
||||
|
@ -51,12 +51,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<article :class="$style.note" @contextmenu.stop="onContextmenu">
|
||||
<header :class="$style.noteHeader">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<MkAvatar v-if="!defaultStore.state.hideAvatarsInNote" :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
|
||||
<MkAvatar v-if="!defaultStore.state.hideAvatarsInNote" :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
|
||||
<div style="display: flex; align-items: center; white-space: nowrap; overflow: hidden;">
|
||||
<div :class="$style.noteHeaderBody">
|
||||
<div>
|
||||
<div :class="$style.noteHeaderName">
|
||||
<MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)">
|
||||
<MkUserName :nowrap="false" :user="appearNote.user"/>
|
||||
<MkUserName :nowrap="true" :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
|
||||
<span v-if="appearNote.user.badgeRoles" :class="$style.badgeRoles">
|
||||
@ -176,6 +176,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'history' }]" @click="tab = 'history'"><i class="ti ti-pencil"></i> {{ i18n.ts.edited }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="tab === 'replies'" :class="$style.tab_replies">
|
||||
@ -213,6 +214,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'history'" :class="$style.tab_history">
|
||||
<div style="display: grid;">
|
||||
<div v-for="(text, index) in appearNote.noteEditHistory" :key="text" :class="$style.historyRoot">
|
||||
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
|
||||
<div :class="$style.historyMain">
|
||||
<div :class="$style.historyHeader">
|
||||
<MkUserName :user="appearNote.user" :nowrap="true"/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<Mfm :text="text.trim()" :author="appearNote.user" :i="$i"/>
|
||||
</div>
|
||||
<CodeDiff
|
||||
:oldString="appearNote.noteEditHistory[index - 1] || ''"
|
||||
:newString="text"
|
||||
:trim="true"
|
||||
:hideHeader="true"
|
||||
diffStyle="char"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
@ -230,6 +255,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { computed, inject, onMounted, ref, shallowRef } from 'vue';
|
||||
import * as mfm from 'cherrypick-mfm-js';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { CodeDiff } from 'v-code-diff';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
@ -810,12 +836,21 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
font-size: 0.95em;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.noteHeaderName {
|
||||
font-weight: bold;
|
||||
line-height: 1.3;
|
||||
margin: 0 .5em 0 0;
|
||||
overflow: scroll;
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--nameHover);
|
||||
@ -842,6 +877,14 @@ onMounted(() => {
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.3;
|
||||
word-wrap: anywhere;
|
||||
overflow: scroll;
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.noteContent {
|
||||
@ -954,6 +997,9 @@ onMounted(() => {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.tab_history {
|
||||
padding: 16px;
|
||||
}
|
||||
.reactionTabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@ -990,6 +1036,10 @@ onMounted(() => {
|
||||
.root {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.noteHeaderBody {
|
||||
max-width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 480px) {
|
||||
@ -1018,6 +1068,36 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.historyRoot {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
overflow: clip;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.historyMain {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.historyHeader {
|
||||
margin-bottom: 2px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.avatar {
|
||||
flex-shrink: 0 !important;
|
||||
display: block !important;
|
||||
margin: 0 10px 0 0 !important;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
border-radius: 8px !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
|
@ -87,13 +87,18 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||
display: block;
|
||||
margin: 0 .5em 0 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
overflow: scroll;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 300px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--nameHover);
|
||||
text-decoration: none;
|
||||
@ -113,9 +118,14 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||
.username {
|
||||
flex-shrink: 9999999;
|
||||
margin: 0 .5em 0 0;
|
||||
overflow: hidden;
|
||||
overflow: scroll;
|
||||
text-overflow: ellipsis;
|
||||
font-size: .95em;
|
||||
max-width: 300px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
@ -150,7 +160,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.name {
|
||||
.name, .username {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
@ -56,5 +56,9 @@ function onModalClosed() {
|
||||
max-height: calc(100% - env(safe-area-inset-bottom));
|
||||
margin: 0 auto auto auto;
|
||||
overflow: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -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 : ''"
|
||||
>
|
||||
<header v-if="formClick" :class="$style.header">
|
||||
<header v-if="showForm" :class="$style.header">
|
||||
<div :class="$style.headerLeft">
|
||||
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
|
||||
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="[$style.account, { [$style.fixed]: fixed }]" class="_button" @click="openAccountMenu">
|
||||
@ -72,16 +72,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
|
||||
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
|
||||
<div :class="[$style.textOuter, { [$style.withCw]: useCw, [$style.formClick]: !formClick }]">
|
||||
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @click="formClick = true" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
||||
<input v-show="useCw && showForm" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
|
||||
<div :class="[$style.textOuter, { [$style.withCw]: useCw, [$style.showForm]: !showForm }]">
|
||||
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted || !$i" :placeholder="placeholder" data-cy-post-form-text @click="formClick" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||
<button v-if="!formClick" v-click-anime class="_button" :class="$style.submit" style="position: absolute; bottom: 0; right: 12px;" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
||||
<button v-if="!showForm" v-click-anime class="_button" :class="$style.submit" style="position: absolute; bottom: 0; right: 12px;" :disabled="!canPost && $i" data-cy-open-post-form-submit @click="$i ? post : signin()">
|
||||
<div :class="$style.submitInner">
|
||||
<template v-if="posted"></template>
|
||||
<template v-else-if="posting"><MkEllipsis/></template>
|
||||
<template v-else>{{ submitText }}</template>
|
||||
<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : defaultStore.state.renameTheButtonInPostFormToNya ? 'ti ti-paw-filled' : 'ti ti-send'"></i>
|
||||
<i v-if="$i" style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : defaultStore.state.renameTheButtonInPostFormToNya ? 'ti ti-paw-filled' : 'ti ti-send'"></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@ -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 : ''"
|
||||
>
|
||||
<footer v-if="formClick" :class="$style.footer">
|
||||
<footer v-if="showForm" :class="$style.footer">
|
||||
<div :class="$style.footerLeft">
|
||||
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
||||
@ -150,6 +150,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { vibrate } from '@/scripts/vibrate.js';
|
||||
import XSigninDialog from '@/components/MkSigninDialog.vue';
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
@ -186,7 +187,7 @@ const cwInputEl = $shallowRef<HTMLInputElement | null>(null);
|
||||
const hashtagsInputEl = $shallowRef<HTMLInputElement | null>(null);
|
||||
const visibilityButton = $shallowRef<HTMLElement | null>(null);
|
||||
|
||||
let formClick = $ref(false);
|
||||
let showForm = $ref(false);
|
||||
|
||||
let posting = $ref(false);
|
||||
let posted = $ref(false);
|
||||
@ -239,7 +240,9 @@ const draftKey = $computed((): string => {
|
||||
});
|
||||
|
||||
const placeholder = $computed((): string => {
|
||||
if (props.renote) {
|
||||
if (!$i) {
|
||||
return i18n.ts._postForm.signinRequiredPlaceholder;
|
||||
} else if (props.renote) {
|
||||
return i18n.ts._postForm.quotePlaceholder;
|
||||
} else if (props.reply) {
|
||||
return i18n.ts._postForm.replyPlaceholder;
|
||||
@ -259,13 +262,15 @@ const placeholder = $computed((): string => {
|
||||
});
|
||||
|
||||
const submitText = $computed((): string => {
|
||||
return props.renote
|
||||
? i18n.ts.quote
|
||||
: props.reply
|
||||
? i18n.ts.reply
|
||||
: defaultStore.state.renameTheButtonInPostFormToNya
|
||||
? i18n.ts.nya
|
||||
: i18n.ts.note;
|
||||
return !$i
|
||||
? i18n.ts.login
|
||||
: props.renote
|
||||
? i18n.ts.quote
|
||||
: props.reply
|
||||
? i18n.ts.reply
|
||||
: defaultStore.state.renameTheButtonInPostFormToNya
|
||||
? i18n.ts.nya
|
||||
: i18n.ts.note;
|
||||
});
|
||||
|
||||
const textLength = $computed((): number => {
|
||||
@ -321,8 +326,6 @@ if (props.reply && props.reply.text != null) {
|
||||
|
||||
// 重複は除外
|
||||
if (text.includes(`${mention} `)) continue;
|
||||
|
||||
text += `${mention} `;
|
||||
}
|
||||
}
|
||||
|
||||
@ -922,6 +925,37 @@ function openAccountMenu(ev: MouseEvent) {
|
||||
}, ev);
|
||||
}
|
||||
|
||||
function formClick() {
|
||||
if ($i) showForm = true;
|
||||
|
||||
if (props.reply && props.reply.text != null) {
|
||||
const ast = mfm.parse(props.reply.text);
|
||||
const otherHost = props.reply.user.host;
|
||||
|
||||
for (const x of extractMentions(ast)) {
|
||||
const mention = x.host ?
|
||||
`@${x.username}@${toASCII(x.host)}` :
|
||||
(otherHost == null || otherHost === host) ?
|
||||
`@${x.username}` :
|
||||
`@${x.username}@${toASCII(otherHost)}`;
|
||||
|
||||
// 自分は除外
|
||||
if ($i.username === x.username && (x.host == null || x.host === host)) continue;
|
||||
|
||||
// 重複は除外
|
||||
if (text.includes(`${mention} `)) continue;
|
||||
|
||||
text += `${mention} `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function signin() {
|
||||
os.popup(XSigninDialog, {
|
||||
autoSet: true,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autofocus) {
|
||||
focus();
|
||||
@ -1229,7 +1263,7 @@ defineExpose({
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
&.formClick {
|
||||
&.showForm {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,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)';
|
||||
|
||||
const getYYYYMMDD = (date: Date) => {
|
||||
const y = date.getFullYear().toString().padStart(2, '0');
|
||||
|
@ -26,7 +26,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkDetailsButton v-if="note.files.length > 0 || note.poll" v-model="showContent" style="width: 100%" :note="note"/>
|
||||
<div v-show="showContent">
|
||||
<div v-if="note.files.length > 0">
|
||||
<MkMediaList v-if="note.disableRightClick" :mediaList="note.files" @contextmenu.prevent/>
|
||||
@ -37,10 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="(isLong || (isMFM && defaultStore.state.collapseDefault)) && collapsed" v-vibrate="5" :class="$style.fade" class="_button" @click="collapsed = false">
|
||||
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
|
||||
<button v-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || note.files.length > 0 || note.poll || defaultStore.state.allMediaNoteCollapse) && collapsed" v-vibrate="5" :class="$style.fade" class="_button" @click="collapsed = false;">
|
||||
<span :class="$style.fadeLabel">
|
||||
{{ i18n.ts.showMore }}
|
||||
<span v-if="note.files.length > 0" :class="$style.label">({{ collapseLabel }})</span>
|
||||
</span>
|
||||
</button>
|
||||
<button v-else-if="(isLong || (isMFM && defaultStore.state.collapseDefault)) && !collapsed" v-vibrate="5" :class="$style.showLess" class="_button" @click="collapsed = true">
|
||||
<button v-else-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || note.files.length > 0 || note.poll || defaultStore.state.allMediaNoteCollapse) && !collapsed" v-vibrate="5" :class="$style.showLess" class="_button" @click="collapsed = true;">
|
||||
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
|
||||
</button>
|
||||
<div v-if="showSubNoteFooterButton">
|
||||
@ -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<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
@ -135,10 +137,16 @@ const canRenote = computed(() => ['public', 'home'].includes(props.note.visibili
|
||||
const isDeleted = ref(false);
|
||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
const showContent = ref(false);
|
||||
const showContent = ref(true);
|
||||
const translation = ref<any>(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,
|
||||
|
@ -40,6 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<MkInfo v-if="!disableShowingAnimatedImages" style="margin-bottom: 15px;" warn>{{ i18n.ts.photosensitiveSeizuresWarning }}</MkInfo>
|
||||
<MkSwitch v-model="disableShowingAnimatedImages">{{ i18n.ts.disableShowingAnimatedImages }}<template #caption>{{ i18n.ts.disableShowingAnimatedImagesDescription }}</template></MkSwitch>
|
||||
<MkRadios v-if="!disableShowingAnimatedImages" v-model="showingAnimatedImages" style="margin-left: 44px;">
|
||||
<option value="always">{{ i18n.ts._showingAnimatedImages.always }}</option>
|
||||
<option value="interaction">{{ i18n.ts._showingAnimatedImages.interaction }}</option>
|
||||
<option value="inactive">{{ i18n.ts._showingAnimatedImages.inactive }}</option>
|
||||
<template #caption>{{ i18n.ts.showingAnimatedImagesDescription }}</template>
|
||||
</MkRadios>
|
||||
</MkFolder>
|
||||
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
|
||||
@ -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'));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -5,12 +5,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="$style.root" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<MkImgWithBlurhash :class="[$style.inner, { [$style.reduceBlurEffect]: !defaultStore.state.useBlurEffect, [$style.noDrag]: noDrag }]" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true" :noDrag="true"/>
|
||||
<MkImgWithBlurhash
|
||||
:class="[$style.inner, { [$style.reduceBlurEffect]: !defaultStore.state.useBlurEffect, [$style.noDrag]: noDrag }]"
|
||||
:src="url"
|
||||
:hash="user?.avatarBlurhash"
|
||||
:cover="true"
|
||||
:onlyAvgColor="true"
|
||||
:noDrag="true"
|
||||
@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 : ''"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import { onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import MkA from '@/components/global/MkA.vue';
|
||||
@ -38,7 +49,10 @@ const bound = $computed(() => props.link
|
||||
? { to: userPage(props.user), target: props.target }
|
||||
: {});
|
||||
|
||||
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
|
||||
let playAnimation = $ref(true);
|
||||
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
|
||||
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
|
||||
? getStaticImageUrl(props.user.avatarUrl)
|
||||
: props.user.avatarUrl);
|
||||
|
||||
@ -47,6 +61,12 @@ function onClick(ev: MouseEvent): void {
|
||||
emit('click', ev);
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
playAnimation = true;
|
||||
clearTimeout(playAnimationTimer);
|
||||
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
}
|
||||
|
||||
let color = $ref<string | undefined>();
|
||||
|
||||
watch(() => props.user.avatarBlurhash, () => {
|
||||
@ -54,6 +74,22 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.addEventListener('mousemove', resetTimer);
|
||||
window.addEventListener('touchstart', resetTimer);
|
||||
window.addEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.removeEventListener('mousemove', resetTimer);
|
||||
window.removeEventListener('touchstart', resetTimer);
|
||||
window.removeEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -5,7 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
|
||||
<MkImgWithBlurhash
|
||||
:class="$style.inner"
|
||||
:src="url"
|
||||
:hash="user?.avatarBlurhash"
|
||||
:cover="true"
|
||||
:onlyAvgColor="true"
|
||||
@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 : ''"
|
||||
/>
|
||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||
<div v-if="user.isCat" :class="[$style.ears]">
|
||||
<div :class="$style.earLeft">
|
||||
@ -27,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import { onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
|
||||
import MkA from './MkA.vue';
|
||||
@ -62,7 +72,10 @@ const bound = $computed(() => props.link
|
||||
? { to: userPage(props.user), target: props.target }
|
||||
: {});
|
||||
|
||||
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
|
||||
let playAnimation = $ref(true);
|
||||
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
|
||||
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
|
||||
? getStaticImageUrl(props.user.avatarUrl)
|
||||
: props.user.avatarUrl);
|
||||
|
||||
@ -71,6 +84,12 @@ function onClick(ev: MouseEvent): void {
|
||||
emit('click', ev);
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
playAnimation = true;
|
||||
clearTimeout(playAnimationTimer);
|
||||
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
}
|
||||
|
||||
let color = $ref<string | undefined>();
|
||||
|
||||
watch(() => props.user.avatarBlurhash, () => {
|
||||
@ -78,6 +97,22 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.addEventListener('mousemove', resetTimer);
|
||||
window.addEventListener('touchstart', resetTimer);
|
||||
window.addEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.removeEventListener('mousemove', resetTimer);
|
||||
window.removeEventListener('touchstart', resetTimer);
|
||||
window.removeEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -5,11 +5,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<span v-if="errored">:{{ customEmojiName }}:</span>
|
||||
<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
|
||||
<img
|
||||
v-else
|
||||
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
||||
:src="url"
|
||||
:alt="alt"
|
||||
:title="alt"
|
||||
decoding="async"
|
||||
@error="errored = true"
|
||||
@load="errored = false"
|
||||
@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 : ''"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
@ -36,6 +49,9 @@ const rawUrl = computed(() => {
|
||||
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
|
||||
});
|
||||
|
||||
let playAnimation = $ref(true);
|
||||
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
|
||||
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
const url = computed(() => {
|
||||
if (rawUrl.value == null) return null;
|
||||
|
||||
@ -48,13 +64,35 @@ const url = computed(() => {
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return defaultStore.reactiveState.disableShowingAnimatedImages.value
|
||||
return defaultStore.reactiveState.disableShowingAnimatedImages.value || (['interaction', 'inactive'].includes(<string>defaultStore.reactiveState.showingAnimatedImages.value) && !playAnimation)
|
||||
? getStaticImageUrl(proxied)
|
||||
: proxied;
|
||||
});
|
||||
|
||||
const alt = computed(() => `:${customEmojiName.value}:`);
|
||||
let errored = $ref(url.value == null);
|
||||
|
||||
function resetTimer() {
|
||||
playAnimation = true;
|
||||
clearTimeout(playAnimationTimer);
|
||||
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.addEventListener('mousemove', resetTimer);
|
||||
window.addEventListener('touchstart', resetTimer);
|
||||
window.addEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.removeEventListener('mousemove', resetTimer);
|
||||
window.removeEventListener('touchstart', resetTimer);
|
||||
window.removeEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -5,14 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="$style.root" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<img :class="$style.inner" :src="url" decoding="async"/>
|
||||
<MkImgWithBlurhash
|
||||
:class="$style.inner"
|
||||
:src="url"
|
||||
:hash="user?.avatarBlurhash"
|
||||
:cover="true"
|
||||
:onlyAvgColor="true"
|
||||
@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 : ''"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import { onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import MkA from '@/components/global/MkA.vue';
|
||||
import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
|
||||
import { acct, userPage } from '@/filters/user.js';
|
||||
@ -37,7 +48,10 @@ const bound = $computed(() => props.link
|
||||
? { to: userPage(props.user), target: props.target }
|
||||
: {});
|
||||
|
||||
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
|
||||
let playAnimation = $ref(true);
|
||||
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
|
||||
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
|
||||
? getStaticImageUrl(props.user.avatarUrl)
|
||||
: props.user.avatarUrl);
|
||||
|
||||
@ -46,6 +60,12 @@ function onClick(ev: MouseEvent): void {
|
||||
emit('click', ev);
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
playAnimation = true;
|
||||
clearTimeout(playAnimationTimer);
|
||||
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
}
|
||||
|
||||
let color = $ref<string | undefined>();
|
||||
|
||||
watch(() => props.user.avatarBlurhash, () => {
|
||||
@ -53,6 +73,22 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.addEventListener('mousemove', resetTimer);
|
||||
window.addEventListener('touchstart', resetTimer);
|
||||
window.addEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.removeEventListener('mousemove', resetTimer);
|
||||
window.removeEventListener('touchstart', resetTimer);
|
||||
window.removeEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -69,6 +69,7 @@ export const ROLE_POLICIES = [
|
||||
'inviteExpirationTime',
|
||||
'canManageCustomEmojis',
|
||||
'canSearchNotes',
|
||||
'canUseTranslator',
|
||||
'canHideAds',
|
||||
'driveCapacityMb',
|
||||
'alwaysMarkNsfw',
|
||||
|
@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="updateAvailable" warn class="info">{{ i18n.ts.newVersionOfClientAvailable }} <MkA to="/admin/update" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
|
||||
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
||||
</div>
|
||||
@ -37,6 +38,7 @@ import * as os from '@/os.js';
|
||||
import { lookupUser } from '@/scripts/lookup-user.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { version } from '@/config.js';
|
||||
|
||||
const isEmpty = (x: string | null) => x == null || x === '';
|
||||
|
||||
@ -61,6 +63,8 @@ let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha
|
||||
let noEmailServer = !instance.enableEmail;
|
||||
let thereIsUnresolvedAbuseReport = $ref(false);
|
||||
let currentPage = $computed(() => router.currentRef.value.child);
|
||||
let updateAvailable = $ref(false);
|
||||
let releasesCherryPick = $ref(null);
|
||||
|
||||
os.api('admin/abuse-user-reports', {
|
||||
state: 'unresolved',
|
||||
@ -69,6 +73,14 @@ os.api('admin/abuse-user-reports', {
|
||||
if (reports.length > 0) thereIsUnresolvedAbuseReport = true;
|
||||
});
|
||||
|
||||
fetch('https://api.github.com/repos/kokonect-link/cherrypick/releases', {
|
||||
method: 'GET',
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
releasesCherryPick = res;
|
||||
if (version < releasesCherryPick[0].tag_name) updateAvailable = true;
|
||||
});
|
||||
|
||||
const NARROW_THRESHOLD = 600;
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
if (entries.length === 0) return;
|
||||
|
@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
|
||||
<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-equal-not"></i> {{ log.info.roleName }}</span>
|
||||
<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
|
||||
<span v-else-if="log.type === 'updateRole'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
|
||||
|
@ -9,6 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps">
|
||||
<FormLink to="/admin/update"><template #icon><i class="ti ti-refresh"></i></template>{{ i18n.ts.cherrypickUpdate }}</FormLink>
|
||||
|
||||
<div class="_panel" style="padding: 16px;">
|
||||
<MkSwitch v-model="enableServerMachineStats">
|
||||
<template #label>{{ i18n.ts.enableServerMachineStats }}</template>
|
||||
@ -57,6 +59,7 @@ import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
|
||||
let enableServerMachineStats: boolean = $ref(false);
|
||||
let enableIdenticonGeneration: boolean = $ref(false);
|
||||
|
@ -127,8 +127,8 @@ onMounted(async () => {
|
||||
|
||||
&.notes {
|
||||
> .icon {
|
||||
background: #86b30026;
|
||||
color: #86b300;
|
||||
background: #ffbcdc26;
|
||||
color: #ffbcdc;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,6 +319,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canUseTranslator'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canUseTranslator.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canUseTranslator.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUseTranslator)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canUseTranslator.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canUseTranslator.value" :disabled="role.policies.canUseTranslator.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canUseTranslator.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>
|
||||
|
@ -111,6 +111,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canSearchNotes'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #suffix>{{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canUseTranslator">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>{{ policies.driveCapacityMb }}MB</template>
|
||||
|
114
packages/frontend/src/pages/admin/update.vue
Normal file
114
packages/frontend/src/pages/admin/update.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<div class="_gaps_m">
|
||||
<template>
|
||||
<FormInfo v-if="version > releasesCherryPick[0].tag_name">{{ i18n.ts.youAreRunningBetaClient }}</FormInfo>
|
||||
<FormInfo v-else-if="version === releasesCherryPick[0].tag_name">{{ i18n.ts.youAreRunningUpToDateClient }}</FormInfo>
|
||||
<FormInfo v-else warn>{{ i18n.ts.newVersionOfClientAvailable }}</FormInfo>
|
||||
</template>
|
||||
|
||||
<FormSection first>
|
||||
<template #label>{{ instanceName }}</template>
|
||||
<MkKeyValue @click="whatIsNewCherryPick">
|
||||
<template #key>{{ i18n.ts.currentVersion }} <i class="ti ti-external-link"></i></template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="version < releasesCherryPick[0].tag_name" style="margin-top: 10px;" @click="whatIsNewLatestCherryPick">
|
||||
<template #key>{{ i18n.ts.latestVersion }} <i class="ti ti-external-link"></i></template>
|
||||
<template v-if="releasesCherryPick" #value>{{ releasesCherryPick[0].tag_name }}</template>
|
||||
<template v-else #value><MkEllipsis/></template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection @click="whatIsNewLatestCherryPick">
|
||||
<template #label>CherryPick <i class="ti ti-external-link"></i></template>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.latestVersion }}</template>
|
||||
<template v-if="releasesCherryPick" #value>{{ releasesCherryPick[0].tag_name }}</template>
|
||||
<template v-else #value><MkEllipsis/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue style="margin: 8px 0 0; color: var(--fgTransparentWeak); font-size: 0.85em;">
|
||||
<template v-if="releasesCherryPick" #value><MkTime :time="releasesCherryPick[0].published_at" mode="detail"/></template>
|
||||
<template v-else #value><MkEllipsis/></template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection @click="whatIsNewLatestMisskey">
|
||||
<template #label>Misskey <i class="ti ti-external-link"></i></template>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.latestVersion }}</template>
|
||||
<template v-if="releasesMisskey" #value>{{ releasesMisskey[0].tag_name }}</template>
|
||||
<template v-else #value><MkEllipsis/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue style="margin: 8px 0 0; color: var(--fgTransparentWeak); font-size: 0.85em;">
|
||||
<template v-if="releasesMisskey" #value><MkTime :time="releasesMisskey[0].published_at" mode="detail"/></template>
|
||||
<template v-else #value><MkEllipsis/></template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import { version, instanceName, basedMisskeyVersion } from '@/config.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import XHeader from '@/pages/admin/_header_.vue';
|
||||
|
||||
let releasesCherryPick = $ref(null);
|
||||
let releasesMisskey = $ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
fetch('https://api.github.com/repos/kokonect-link/cherrypick/releases', {
|
||||
method: 'GET',
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
releasesCherryPick = res;
|
||||
});
|
||||
|
||||
fetch('https://api.github.com/repos/misskey-dev/misskey/releases', {
|
||||
method: 'GET',
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
releasesMisskey = res;
|
||||
});
|
||||
});
|
||||
|
||||
const whatIsNewCherryPick = () => {
|
||||
window.open(`https://github.com/kokonect-link/cherrypick/blob/develop/CHANGELOG_CHERRYPICK.md#${version.replace(/\./g, '')}`, '_blank');
|
||||
};
|
||||
|
||||
const whatIsNewLatestCherryPick = () => {
|
||||
window.open(`https://github.com/kokonect-link/cherrypick/blob/develop/CHANGELOG_CHERRYPICK.md#${releasesCherryPick[0].tag_name.replace(/\./g, '')}`, '_blank');
|
||||
};
|
||||
|
||||
const whatIsNewMisskey = () => {
|
||||
window.open(`https://misskey-hub.net/docs/releases.html#_${basedMisskeyVersion.replace(/\./g, '-')}`, '_blank');
|
||||
};
|
||||
|
||||
const whatIsNewLatestMisskey = () => {
|
||||
window.open(`https://misskey-hub.net/docs/releases.html#_${releasesMisskey[0].tag_name.replace(/\./g, '-')}`, '_blank');
|
||||
};
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.cherrypickUpdate,
|
||||
icon: 'ti ti-refresh',
|
||||
});
|
||||
</script>
|
@ -67,6 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<option value="medium">{{ i18n.ts.medium }}</option>
|
||||
<option value="large">{{ i18n.ts.large }}</option>
|
||||
</MkRadios>
|
||||
<MkSwitch v-model="hideAvatarsInNote">{{ i18n.ts.hideAvatarsInNote }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||
<MkSwitch v-model="enableAbsoluteTime">{{ i18n.ts.enableAbsoluteTime }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||
<MkSwitch v-model="enableMarkByDate" :disabled="defaultStore.state.enableAbsoluteTime">{{ i18n.ts.enableMarkByDate }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||
<MkSwitch v-model="showSubNoteFooterButton">{{ i18n.ts.showSubNoteFooterButton }}<template #caption>{{ i18n.ts.showSubNoteFooterButtonDescription }}</template> <span class="_beta">CherryPick</span></MkSwitch>
|
||||
@ -74,6 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="showReplyInNotification">{{ i18n.ts.showReplyInNotification }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||
<MkSwitch v-model="renoteQuoteButtonSeparation">{{ i18n.ts.renoteQuoteButtonSeparation }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||
<MkSwitch v-model="showFixedPostFormInReplies">{{ i18n.ts.showFixedPostFormInReplies }}<template #caption>{{ i18n.ts.showFixedPostFormInRepliesDescription }}</template> <span class="_beta">CherryPick</span></MkSwitch>
|
||||
<MkSwitch v-model="allMediaNoteCollapse">{{ i18n.ts.allMediaNoteCollapse }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||
</div>
|
||||
|
||||
<MkSelect v-model="instanceTicker">
|
||||
@ -130,10 +132,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="reduceAnimation">{{ i18n.ts.reduceUiAnimation }}</MkSwitch>
|
||||
<MkSwitch v-model="useBlurEffect">{{ i18n.ts.useBlurEffect }}</MkSwitch>
|
||||
<MkSwitch v-model="useBlurEffectForModal">{{ i18n.ts.useBlurEffectForModal }}</MkSwitch>
|
||||
<MkSwitch v-model="disableShowingAnimatedImages">{{ i18n.ts.disableShowingAnimatedImages }}<template #caption>{{ i18n.ts.disableShowingAnimatedImagesDescription }}</template></MkSwitch>
|
||||
<MkSwitch v-model="disableShowingAnimatedImages">{{ i18n.ts.disableShowingAnimatedImages }}<template #caption><i class="ti ti-alert-triangle" style="color: var(--warn);"></i> {{ i18n.ts.disableShowingAnimatedImagesDescription }}</template></MkSwitch>
|
||||
<MkSelect v-if="!disableShowingAnimatedImages" v-model="showingAnimatedImages" style="margin-left: 44px;">
|
||||
<option value="always">{{ i18n.ts._showingAnimatedImages.always }}</option>
|
||||
<option value="interaction">{{ i18n.ts._showingAnimatedImages.interaction }}</option>
|
||||
<option value="inactive">{{ i18n.ts._showingAnimatedImages.inactive }}</option>
|
||||
<template #caption>{{ i18n.ts.showingAnimatedImagesDescription }}</template>
|
||||
</MkSelect>
|
||||
<MkSwitch v-model="highlightSensitiveMedia">{{ i18n.ts.highlightSensitiveMedia }}</MkSwitch>
|
||||
<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
|
||||
<MkSwitch v-model="hideAvatarsInNote">{{ i18n.ts.hideAvatarsInNote }} <span class="_beta">CherryPick</span></MkSwitch>
|
||||
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
|
||||
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
|
||||
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
|
||||
@ -359,6 +366,8 @@ const infoButtonForNoteActionsEnabled = computed(defaultStore.makeGetterSetter('
|
||||
const showReplyInNotification = computed(defaultStore.makeGetterSetter('showReplyInNotification'));
|
||||
const renoteQuoteButtonSeparation = computed(defaultStore.makeGetterSetter('renoteQuoteButtonSeparation'));
|
||||
const showFixedPostFormInReplies = computed(defaultStore.makeGetterSetter('showFixedPostFormInReplies'));
|
||||
const showingAnimatedImages = computed(defaultStore.makeGetterSetter('showingAnimatedImages'));
|
||||
const allMediaNoteCollapse = computed(defaultStore.makeGetterSetter('allMediaNoteCollapse'));
|
||||
|
||||
watch(lang, () => {
|
||||
miLocalStorage.setItem('lang', lang.value as string);
|
||||
@ -415,6 +424,8 @@ watch([
|
||||
showReplyInNotification,
|
||||
renoteQuoteButtonSeparation,
|
||||
showFixedPostFormInReplies,
|
||||
showingAnimatedImages,
|
||||
allMediaNoteCollapse,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</mkstickycontainer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -84,6 +84,7 @@ import MkFolder from '@/components/MkFolder.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { signout, $i } from '@/account.js';
|
||||
@ -91,7 +92,6 @@ import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
|
||||
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
|
||||
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
|
||||
|
@ -71,6 +71,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
||||
'loadRawImages',
|
||||
'imageNewTab',
|
||||
'disableShowingAnimatedImages',
|
||||
'showingAnimatedImages',
|
||||
'emojiStyle',
|
||||
'disableDrawer',
|
||||
'useBlurEffectForModal',
|
||||
|
@ -82,7 +82,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;
|
||||
|
@ -16,7 +16,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:class="$style.img"
|
||||
:to="notePage(image.note)"
|
||||
>
|
||||
<ImgWithBlurhash :hash="image.file.blurhash" :src="thumbnail(image.file)" :title="image.file.name"/>
|
||||
<ImgWithBlurhash
|
||||
:hash="image.file.blurhash"
|
||||
:src="thumbnail(image.file)"
|
||||
:title="image.file.name"
|
||||
@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 : ''"
|
||||
/>
|
||||
</MkA>
|
||||
</div>
|
||||
<p v-if="!fetching && images.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
|
||||
@ -25,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
@ -45,13 +53,28 @@ let images = $ref<{
|
||||
file: Misskey.entities.DriveFile;
|
||||
}[]>([]);
|
||||
|
||||
let playAnimation = $ref(true);
|
||||
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
|
||||
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
function thumbnail(image: Misskey.entities.DriveFile): string {
|
||||
return defaultStore.state.disableShowingAnimatedImages
|
||||
return defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
|
||||
? getStaticImageUrl(image.url)
|
||||
: image.thumbnailUrl;
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
playAnimation = true;
|
||||
clearTimeout(playAnimationTimer);
|
||||
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.addEventListener('mousemove', resetTimer);
|
||||
window.addEventListener('touchstart', resetTimer);
|
||||
window.addEventListener('touchend', resetTimer);
|
||||
}
|
||||
|
||||
const image = [
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
@ -78,6 +101,14 @@ onMounted(() => {
|
||||
fetching = false;
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.removeEventListener('mousemove', resetTimer);
|
||||
window.removeEventListener('touchstart', resetTimer);
|
||||
window.removeEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -459,6 +459,10 @@ export const routes = [{
|
||||
path: '/invites',
|
||||
name: 'invites',
|
||||
component: page(() => import('./pages/admin/invites.vue')),
|
||||
}, {
|
||||
path: '/update',
|
||||
name: 'update',
|
||||
component: page(() => import('./pages/admin/update.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('./pages/_empty_.vue')),
|
||||
|
@ -10,7 +10,7 @@ import * as os from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { lang } from '@/config.js';
|
||||
import { url, lang } from '@/config.js';
|
||||
|
||||
export function createAiScriptEnv(opts) {
|
||||
const table = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
@ -23,6 +23,7 @@ export function createAiScriptEnv(opts) {
|
||||
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
|
||||
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
|
||||
LOCALE: values.STR(lang),
|
||||
SERVER_URL: values.STR(url),
|
||||
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
|
||||
await os.alert({
|
||||
type: type ? type.value : 'info',
|
||||
|
@ -331,7 +331,7 @@ export function getNoteMenu(props: {
|
||||
text: i18n.ts.share,
|
||||
action: share,
|
||||
},
|
||||
instance.translatorAvailable ? {
|
||||
$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
|
||||
icon: 'ti ti-language-hiragana',
|
||||
text: i18n.ts.translate,
|
||||
action: translate,
|
||||
|
@ -21,6 +21,8 @@ export function useTooltip(
|
||||
|
||||
let changeShowingState: (() => void) | null;
|
||||
|
||||
let autoHidingTimer;
|
||||
|
||||
const open = () => {
|
||||
close();
|
||||
if (!isHovering) return;
|
||||
@ -33,6 +35,16 @@ export function useTooltip(
|
||||
changeShowingState = () => {
|
||||
showing.value = false;
|
||||
};
|
||||
|
||||
autoHidingTimer = window.setInterval(() => {
|
||||
if (!document.body.contains(elRef.value)) {
|
||||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
window.clearTimeout(timeoutId);
|
||||
close();
|
||||
window.clearInterval(autoHidingTimer);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
@ -53,6 +65,7 @@ export function useTooltip(
|
||||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
window.clearTimeout(timeoutId);
|
||||
window.clearInterval(autoHidingTimer);
|
||||
close();
|
||||
};
|
||||
|
||||
@ -67,6 +80,7 @@ export function useTooltip(
|
||||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
window.clearTimeout(timeoutId);
|
||||
window.clearInterval(autoHidingTimer);
|
||||
close();
|
||||
};
|
||||
|
||||
|
@ -234,6 +234,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: window.matchMedia('(prefers-reduced-motion)').matches,
|
||||
},
|
||||
showingAnimatedImages: {
|
||||
where: 'device',
|
||||
default: /mobile|iphone|android/.test(navigator.userAgent.toLowerCase()) ? 'inactive' : 'always' as 'always' | 'interaction' | 'inactive',
|
||||
},
|
||||
emojiStyle: {
|
||||
where: 'device',
|
||||
default: 'twemoji', // twemoji / fluentEmoji / native
|
||||
@ -441,6 +445,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
allMediaNoteCollapse: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
|
||||
// - Settings/Timeline
|
||||
enableHomeTimeline: {
|
||||
|
@ -57,6 +57,7 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { version } from '@/config.js';
|
||||
|
||||
const menu = toRef(defaultStore.state, 'menu');
|
||||
const otherMenuItemIndicated = computed(() => {
|
||||
@ -67,6 +68,7 @@ const otherMenuItemIndicated = computed(() => {
|
||||
return false;
|
||||
});
|
||||
let controlPanelIndicated = $ref(false);
|
||||
let releasesCherryPick = $ref(null);
|
||||
|
||||
os.api('admin/abuse-user-reports', {
|
||||
state: 'unresolved',
|
||||
@ -75,6 +77,14 @@ os.api('admin/abuse-user-reports', {
|
||||
if (reports.length > 0) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
fetch('https://api.github.com/repos/kokonect-link/cherrypick/releases', {
|
||||
method: 'GET',
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
releasesCherryPick = res;
|
||||
if (version < releasesCherryPick[0].tag_name) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
function openAccountMenu(ev: MouseEvent) {
|
||||
openAccountMenu_({
|
||||
withExtraOperation: true,
|
||||
|
@ -69,6 +69,7 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { version } from '@/config.js';
|
||||
|
||||
const iconOnly = ref(false);
|
||||
|
||||
@ -81,6 +82,7 @@ const otherMenuItemIndicated = computed(() => {
|
||||
return false;
|
||||
});
|
||||
let controlPanelIndicated = $ref(false);
|
||||
let releasesCherryPick = $ref(null);
|
||||
|
||||
os.api('admin/abuse-user-reports', {
|
||||
state: 'unresolved',
|
||||
@ -89,6 +91,14 @@ os.api('admin/abuse-user-reports', {
|
||||
if (reports.length > 0) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
fetch('https://api.github.com/repos/kokonect-link/cherrypick/releases', {
|
||||
method: 'GET',
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
releasesCherryPick = res;
|
||||
if (version < releasesCherryPick[0].tag_name) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
const calcViewState = () => {
|
||||
iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
|
||||
};
|
||||
|
@ -60,6 +60,7 @@ import MkButton from '@/components/MkButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { version } from '@/config.js';
|
||||
|
||||
const WINDOW_THRESHOLD = 1400;
|
||||
|
||||
@ -78,6 +79,7 @@ let el = $shallowRef<HTMLElement>();
|
||||
let iconOnly = $ref(false);
|
||||
let settingsWindowed = $ref(false);
|
||||
let controlPanelIndicated = $ref(false);
|
||||
let releasesCherryPick = $ref(null);
|
||||
|
||||
os.api('admin/abuse-user-reports', {
|
||||
state: 'unresolved',
|
||||
@ -86,6 +88,14 @@ os.api('admin/abuse-user-reports', {
|
||||
if (reports.length > 0) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
fetch('https://api.github.com/repos/kokonect-link/cherrypick/releases', {
|
||||
method: 'GET',
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
releasesCherryPick = res;
|
||||
if (version < releasesCherryPick[0].tag_name) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
function calcViewState() {
|
||||
iconOnly = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon');
|
||||
settingsWindowed = (window.innerWidth > WINDOW_THRESHOLD);
|
||||
|
@ -9,12 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
||||
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId"/>
|
||||
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :with-renotes="withRenotes" :with-replies="withReplies"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn, Column } from './deck-store';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
@ -27,11 +27,24 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const withRenotes = $ref(props.column.withRenotes ?? true);
|
||||
const withReplies = $ref(props.column.withReplies ?? true);
|
||||
|
||||
if (props.column.listId == null) {
|
||||
setList();
|
||||
}
|
||||
|
||||
watch($$(withRenotes), v => {
|
||||
updateColumn(props.column.id, {
|
||||
withRenotes: v,
|
||||
});
|
||||
});
|
||||
|
||||
watch($$(withReplies), v => {
|
||||
updateColumn(props.column.id, {
|
||||
withReplies: v,
|
||||
});
|
||||
});
|
||||
async function setList() {
|
||||
const lists = await os.api('users/lists/list');
|
||||
const { canceled, result: list } = await os.select({
|
||||
@ -62,5 +75,15 @@ const menu = [
|
||||
text: i18n.ts.editList,
|
||||
action: editList,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
ref: $$(withRenotes),
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.withReplies,
|
||||
ref: $$(withReplies),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
@ -61,6 +61,7 @@ import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { version } from '@/config.js';
|
||||
|
||||
const menu = toRef(defaultStore.state, 'menu');
|
||||
const otherMenuItemIndicated = computed(() => {
|
||||
@ -71,6 +72,7 @@ const otherMenuItemIndicated = computed(() => {
|
||||
return false;
|
||||
});
|
||||
let controlPanelIndicated = $ref(false);
|
||||
let releasesCherryPick = $ref(null);
|
||||
|
||||
os.api('admin/abuse-user-reports', {
|
||||
state: 'unresolved',
|
||||
@ -79,6 +81,14 @@ os.api('admin/abuse-user-reports', {
|
||||
if (reports.length > 0) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
fetch('https://api.github.com/repos/kokonect-link/cherrypick/releases', {
|
||||
method: 'GET',
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
releasesCherryPick = res;
|
||||
if (version < releasesCherryPick[0].tag_name) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
function openAccountMenu(ev: MouseEvent) {
|
||||
openAccountMenu_({
|
||||
withExtraOperation: true,
|
||||
|
@ -76,6 +76,7 @@ import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { version } from '@/config.js';
|
||||
|
||||
const iconOnly = ref(false);
|
||||
|
||||
@ -88,6 +89,7 @@ const otherMenuItemIndicated = computed(() => {
|
||||
return false;
|
||||
});
|
||||
let controlPanelIndicated = $ref(false);
|
||||
let releasesCherryPick = $ref(null);
|
||||
|
||||
os.api('admin/abuse-user-reports', {
|
||||
state: 'unresolved',
|
||||
@ -96,6 +98,14 @@ os.api('admin/abuse-user-reports', {
|
||||
if (reports.length > 0) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
fetch('https://api.github.com/repos/kokonect-link/cherrypick/releases', {
|
||||
method: 'GET',
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
releasesCherryPick = res;
|
||||
if (version < releasesCherryPick[0].tag_name) controlPanelIndicated = true;
|
||||
});
|
||||
|
||||
const calcViewState = () => {
|
||||
iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
|
||||
};
|
||||
|
@ -15,6 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
v-for="(image, i) in images" :key="i"
|
||||
:class="$style.img"
|
||||
:style="`background-image: url(${thumbnail(image)})`"
|
||||
@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 : ''"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -22,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||
import { GetFormResultType } from '@/scripts/form.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
@ -67,12 +71,21 @@ const onDriveFileCreated = (file) => {
|
||||
}
|
||||
};
|
||||
|
||||
let playAnimation = $ref(true);
|
||||
if (defaultStore.state.showingAnimatedImages === 'interaction') playAnimation = false;
|
||||
let playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
const thumbnail = (image: any): string => {
|
||||
return defaultStore.state.disableShowingAnimatedImages
|
||||
return defaultStore.state.disableShowingAnimatedImages || (['interaction', 'inactive'].includes(<string>defaultStore.state.showingAnimatedImages) && !playAnimation)
|
||||
? getStaticImageUrl(image.url)
|
||||
: image.thumbnailUrl;
|
||||
};
|
||||
|
||||
function resetTimer() {
|
||||
playAnimation = true;
|
||||
clearTimeout(playAnimationTimer);
|
||||
playAnimationTimer = setTimeout(() => playAnimation = false, 5000);
|
||||
}
|
||||
|
||||
os.api('drive/stream', {
|
||||
type: 'image/*',
|
||||
limit: 9,
|
||||
@ -82,7 +95,22 @@ os.api('drive/stream', {
|
||||
});
|
||||
|
||||
connection.on('driveFileCreated', onDriveFileCreated);
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.addEventListener('mousemove', resetTimer);
|
||||
window.addEventListener('touchstart', resetTimer);
|
||||
window.addEventListener('touchend', resetTimer);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (defaultStore.state.showingAnimatedImages === 'inactive') {
|
||||
window.removeEventListener('mousemove', resetTimer);
|
||||
window.removeEventListener('touchstart', resetTimer);
|
||||
window.removeEventListener('touchend', resetTimer);
|
||||
}
|
||||
|
||||
connection.dispose();
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user