1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-12-18 00:29:04 +09:00
This commit is contained in:
NoriDev 2023-10-03 14:27:12 +09:00
commit 7e11a01c04
72 changed files with 998 additions and 103 deletions

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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
View File

@ -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;

View File

@ -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: "チャンネルに投稿..."

View File

@ -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: "채널에 게시하기..."

View File

@ -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: "อัปเดตโฆษณาแล้ว"

View File

@ -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: "更新了广告"

View File

@ -1136,6 +1136,8 @@ authentication: "驗證"
authenticationRequiredToContinue: "請於繼續前完成驗證"
dateAndTime: "日期與時間"
showRenotes: "顯示轉發貼文"
edited: "已編輯"
mutualFollow: "互相追隨"
_announcement:
forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
@ -2217,3 +2219,6 @@ _moderationLogTypes:
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
resolveAbuseReport: "解決檢舉"
createInvitation: "建立邀請碼"
createAd: "建立廣告"
deleteAd: "刪除廣告"
updateAd: "更新廣告"

View File

@ -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",

View 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"`);
}
}

View File

@ -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 {

View File

@ -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;

View File

@ -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;
}

View File

@ -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)),

View File

@ -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,

View File

@ -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,

View File

@ -29,6 +29,13 @@ export class MiNote {
})
public updatedAt: Date | null;
@Column('varchar', {
length: 3000,
array: true,
default: '{}',
})
public noteEditHistory: string[];
@Index()
@Column({
...id(),

View File

@ -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,

View File

@ -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) {

View File

@ -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,

View File

@ -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,

View File

@ -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');

View File

@ -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', {

View File

@ -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;

View File

@ -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) {

View File

@ -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')

View File

@ -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, []);

View File

@ -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)

View File

@ -178,6 +178,7 @@ export type Note = {
id: ID;
createdAt: DateString;
updatedAt?: DateString | null;
noteEditHistory: string[];
text: string | null;
cw: string | null;
user: User;

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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),

View File

@ -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/>
<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;

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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,7 +262,9 @@ const placeholder = $computed((): string => {
});
const submitText = $computed((): string => {
return props.renote
return !$i
? i18n.ts.login
: props.renote
? i18n.ts.quote
: props.reply
? i18n.ts.reply
@ -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;
}
}

View File

@ -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');

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -69,6 +69,7 @@ export const ROLE_POLICIES = [
'inviteExpirationTime',
'canManageCustomEmojis',
'canSearchNotes',
'canUseTranslator',
'canHideAds',
'driveCapacityMb',
'alwaysMarkNsfw',

View File

@ -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;

View File

@ -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>

View File

@ -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);

View File

@ -127,8 +127,8 @@ onMounted(async () => {
&.notes {
> .icon {
background: #86b30026;
color: #86b300;
background: #ffbcdc26;
color: #ffbcdc;
}
}

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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();
});

View File

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</MkSpacer>
</mkstickycontainer>
</MkStickyContainer>
</template>
<script setup lang="ts">

View File

@ -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'));

View File

@ -71,6 +71,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'loadRawImages',
'imageNewTab',
'disableShowingAnimatedImages',
'showingAnimatedImages',
'emojiStyle',
'disableDrawer',
'useBlurEffectForModal',

View File

@ -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;

View File

@ -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>

View File

@ -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')),

View File

@ -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',

View File

@ -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,

View File

@ -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();
};

View File

@ -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: {

View File

@ -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,

View File

@ -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');
};

View File

@ -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);

View File

@ -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>

View File

@ -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,

View File

@ -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');
};

View File

@ -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();
});