Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
449cbebe16
|
@ -21,15 +21,23 @@
|
|||
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
||||
|
||||
### Client
|
||||
- Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加
|
||||
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
||||
- Enhance: ユーザーのRawデータを表示するページが復活
|
||||
- Enhance: リアクション選択時に音を鳴らせるように
|
||||
- Enhance: サウンドにドライブのファイルを使用できるように
|
||||
- Enhance: ナビゲーションバーに項目「キャッシュを削除」を追加
|
||||
- Enhance: Shareページで投稿を完了すると、親ウィンドウ(親フレーム)にpostMessageするように
|
||||
- Enhance: チャンネル、クリップ、ページ、Play、ギャラリーにURLのコピーボタンを設置 #11305
|
||||
- Enhance: ノートプレビューに「内容を隠す」が反映されるように
|
||||
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
||||
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
|
||||
- Enhance: 絵文字の詳細ページに記載される情報を追加
|
||||
- Fix: コードエディタが正しく表示されない問題を修正
|
||||
- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正
|
||||
- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正
|
||||
- Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305
|
||||
- Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470
|
||||
|
||||
### Server
|
||||
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
|
||||
|
|
|
@ -40,12 +40,18 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE
|
|||
- 위치 조정
|
||||
- 크기 조정
|
||||
- 불투명도 조정
|
||||
- Feat: 노트를 클릭하여 자세히 볼 수 있음
|
||||
- Change: 노트를 번역할 때 유저가 고양이로 설정되어 있으면 nyaize를 적용
|
||||
- Revert: 사용자 통계 표시 기능 제거 ([MisskeyIO/misskey@114c7fe6](https://github.com/MisskeyIO/misskey/commit/114c7fe6b37dd6bddbcd9d92406f8b13bf688e8b))
|
||||
|
||||
### Client
|
||||
- Enhance: 사운드 설정을 기본값으로 복원하거나 저장할 때 확실하게 표시함
|
||||
- Enhance: 리모트 서버와 동일한 이모지가 존재하지 않는 경우 '이모지 복사'를 비활성화함
|
||||
- Enhance: 아이콘 장식을 바로 업로드 하거나 드라이브에서 불러올 수 있음 ([Secineralyr/misskey.dream@e358212d](https://github.com/Secineralyr/misskey.dream/commit/e358212da93256749e31d9e0ca9dd2ed37fd548e), [Secineralyr/misskey.dream@52592fea](https://github.com/Secineralyr/misskey.dream/commit/52592fea52684497ba7e07f173aac2b1083afcb1))
|
||||
- Enhance: 클라이언트 언어와 노트 본문의 언어가 같으면 번역 버튼을 표시하지 않음
|
||||
- Enhance: 진동 개선
|
||||
- 진동을 사용할 수 없는 환경에서 스위치를 조작할 수 없도록 비활성화
|
||||
- 진동을 사용할 수 없는 이유를 보다 명확하게 표시하도록 개선
|
||||
- Fix: '모달 배경색 제거' 옵션이 이모지 피커에 반영되지 않음
|
||||
- Fix: 열람 주의로 설정된 노트의 반응이 더 보기를 눌러야 표시됨
|
||||
|
||||
|
|
|
@ -67,8 +67,8 @@ RUN apt-get update \
|
|||
&& corepack enable \
|
||||
&& groupadd -g "${GID}" cherrypick \
|
||||
&& useradd -l -u "${UID}" -g "${GID}" -m -d /cherrypick cherrypick \
|
||||
&& find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
|
||||
&& find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \
|
||||
&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
|
||||
&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
_lang_: "English"
|
||||
cannotBeUsedFunc: "This feature is currently unavailable."
|
||||
maxinumLayerError: "You cannot stack more than 6 layers. Please delete other layers."
|
||||
layer: "Layer"
|
||||
Xcoordinate: "X-Coordinate"
|
||||
|
@ -1317,6 +1318,8 @@ _cherrypick:
|
|||
postFormVisibilityHotkeyDescription: "When writing a note, press Ctrl(control) + Shift to switch the visibility range. The hotkey to make it Local only is Ctrl(command or control) + Alt(option)."
|
||||
showRenoteConfirmPopup: "Show confirmation popup when renote"
|
||||
showRenoteConfirmPopupDescription: "This setting must have the \"General - Show renote and quote buttons separately\" setting turned on."
|
||||
expandOnNoteClick: "Open note on click"
|
||||
expandOnNoteClickDescription: "If disabled, you can still open 'Details' in the notes menu or by clicking the timestamp."
|
||||
displayHeaderNavBarWhenScroll: "Show elements when scrolling (header, floating buttons, navigation bar)"
|
||||
_displayHeaderNavBarWhenScroll:
|
||||
all: "Display all"
|
||||
|
|
6
locales/index.d.ts
vendored
6
locales/index.d.ts
vendored
|
@ -3,6 +3,7 @@
|
|||
// Do not edit this file directly.
|
||||
export interface Locale {
|
||||
"_lang_": string;
|
||||
"cannotBeUsedFunc": string;
|
||||
"maxinumLayerError": string;
|
||||
"layer": string;
|
||||
"Xcoordinate": string;
|
||||
|
@ -1129,6 +1130,8 @@ export interface Locale {
|
|||
"sensitiveWords": string;
|
||||
"sensitiveWordsDescription": string;
|
||||
"sensitiveWordsDescription2": string;
|
||||
"hiddenTags": string;
|
||||
"hiddenTagsDescription": string;
|
||||
"notesSearchNotAvailable": string;
|
||||
"license": string;
|
||||
"unfavoriteConfirm": string;
|
||||
|
@ -1330,6 +1333,8 @@ export interface Locale {
|
|||
"postFormVisibilityHotkeyDescription": string;
|
||||
"showRenoteConfirmPopup": string;
|
||||
"showRenoteConfirmPopupDescription": string;
|
||||
"expandOnNoteClick": string;
|
||||
"expandOnNoteClickDescription": string;
|
||||
"displayHeaderNavBarWhenScroll": string;
|
||||
"_displayHeaderNavBarWhenScroll": {
|
||||
"all": string;
|
||||
|
@ -2420,6 +2425,7 @@ export interface Locale {
|
|||
"chooseList": string;
|
||||
};
|
||||
"clicker": string;
|
||||
"birthdayFollowings": string;
|
||||
};
|
||||
"_cw": {
|
||||
"hide": string;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
_lang_: "日本語"
|
||||
|
||||
cannotBeUsedFunc: "この機能は現在使用できません。"
|
||||
maxinumLayerError: "6枚以上重ねることはできません。他のレイヤーを削除してください。"
|
||||
layer: "レイヤー"
|
||||
Xcoordinate: "X座標"
|
||||
|
@ -1126,6 +1127,8 @@ resetPasswordConfirm: "パスワードリセットしますか?"
|
|||
sensitiveWords: "センシティブワード"
|
||||
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
||||
hiddenTags: "非表示ハッシュタグ"
|
||||
hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。"
|
||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||
license: "ライセンス"
|
||||
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||
|
@ -1328,6 +1331,8 @@ _cherrypick:
|
|||
postFormVisibilityHotkeyDescription: "ノートを作成する際、Ctrl(control) + Shiftキーを押すと公開範囲を切り替えることができます。ローカルのみショートカットキーは、Ctrl(commandまたはcontrol) + Alt(option)キーです。"
|
||||
showRenoteConfirmPopup: "Renoteするときに確認ポップアップを表示"
|
||||
showRenoteConfirmPopupDescription: "この設定は「全般 - リノートと引用ボタンを分けて表示する」設定がオンになっている必要があります。"
|
||||
expandOnNoteClick: "クリックでノートの詳細を開く"
|
||||
expandOnNoteClickDescription: "オフの場合、ノートメニューの[詳細]をクリックするか、日付をクリックして開けます。"
|
||||
displayHeaderNavBarWhenScroll: "スクロール時の要素表示(ヘッダー、フローティングボタン、ナビゲーションバー)"
|
||||
_displayHeaderNavBarWhenScroll:
|
||||
all: "全て表示"
|
||||
|
@ -2321,6 +2326,7 @@ _widgets:
|
|||
_userList:
|
||||
chooseList: "リストを選択"
|
||||
clicker: "クリッカー"
|
||||
birthdayFollowings: "今日誕生日のユーザー"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
_lang_: "한국어"
|
||||
cannotBeUsedFunc: "이 기능은 현재 사용할 수 없어요."
|
||||
maxinumLayerError: "레이어는 6장 이상 겹칠 수 없어요. 다른 레이어를 삭제해 주세요."
|
||||
layer: "레이어"
|
||||
Xcoordinate: "X 좌표"
|
||||
|
@ -632,7 +633,7 @@ sound: "소리"
|
|||
vibrations: "진동"
|
||||
soundsAndVibrations: "소리 및 진동"
|
||||
playVibrations: "진동 재생"
|
||||
playVibrationsDescription: "이 기능은 iOS/iPadOS에서는 지원되지 않아요."
|
||||
playVibrationsDescription: "이 기능을 지원하지 않는 브라우저이거나 디바이스가 iOS/iPadOS인 경우에는 지원되지 않아요."
|
||||
listen: "듣기"
|
||||
none: "없음"
|
||||
showInPage: "페이지로 보기"
|
||||
|
@ -1312,9 +1313,11 @@ _cherrypick:
|
|||
useEnterToSend: "Enter 키를 눌러 보내기"
|
||||
useEnterToSendDescription: "옵션을 활성화하면 줄 바꿈은 Shift + Enter 키로 할 수 있어요. 대화를 전송할 때는 옵션의 영향을 받지 않아요."
|
||||
postFormVisibilityHotkey: "단축키로 공개 범위 전환하기"
|
||||
postFormVisibilityHotkeyDescription: "페잇을 작성할 때, Ctrl(control) + Shift 키를 누르면 공개 범위를 전환할 수 있어요. 로컬에만 보이게 하는 단축키는 Ctrl(command 또는 control) + Alt(option) 키에요."
|
||||
postFormVisibilityHotkeyDescription: "페잇을 작성할 때, Ctrl(control) + Shift 키를 누르면 공개 범위를 전환할 수 있습니다. 연합을 끄려면 Ctrl(command 또는 control) + Alt(option) 키를 누르세요."
|
||||
showRenoteConfirmPopup: "리페잇할 때 확인 팝업 표시"
|
||||
showRenoteConfirmPopupDescription: "이 설정은 '일반 - 리페잇과 인용 버튼을 분리해서 표시하기' 설정이 켜져 있어야 해요."
|
||||
showRenoteConfirmPopupDescription: "이 설정은 '일반 - 리페잇과 인용 버튼을 분리해서 표시하기' 설정이 켜져 있어야 작동합니다."
|
||||
expandOnNoteClick: "페잇을 클릭하여 자세히 표시"
|
||||
expandOnNoteClickDescription: "비활성화한 경우에도 페잇 메뉴에서 '자세히'를 클릭하거나 작성된 시간을 클릭해 열 수 있습니다."
|
||||
displayHeaderNavBarWhenScroll: "스크롤 시 요소 표시 (헤더, 플로팅 버튼, 탐색 모음)"
|
||||
_displayHeaderNavBarWhenScroll:
|
||||
all: "모두 표시"
|
||||
|
|
16
packages/backend/migration/1700902349231-add-bday-index.js
Normal file
16
packages/backend/migration/1700902349231-add-bday-index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AddBdayIndex1700902349231 {
|
||||
name = 'AddBdayIndex1700902349231'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (SUBSTR("birthday", 6, 5))`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`);
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ export class MiUserProfile {
|
|||
})
|
||||
public location: string | null;
|
||||
|
||||
@Index()
|
||||
@Column('char', {
|
||||
length: 10, nullable: true,
|
||||
comment: 'The birthday (YYYY-MM-DD) of the User.',
|
||||
|
|
|
@ -208,6 +208,10 @@ export const packedNoteSchema = {
|
|||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
clippedCount: {
|
||||
type: 'number',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
|
||||
myReaction: {
|
||||
type: 'object',
|
||||
|
|
|
@ -33,13 +33,7 @@ export const meta = {
|
|||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
},
|
||||
ref: 'InviteCode',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -21,6 +21,7 @@ export const meta = {
|
|||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'InviteCode',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -31,13 +31,7 @@ export const meta = {
|
|||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
},
|
||||
ref: 'InviteCode',
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import type { RegistrationTicketsRepository } from '@/models/_.js';
|
|||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
@ -23,6 +22,7 @@ export const meta = {
|
|||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'InviteCode',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -42,6 +42,12 @@ export const meta = {
|
|||
code: 'FORBIDDEN',
|
||||
id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba',
|
||||
},
|
||||
|
||||
birthdayInvalid: {
|
||||
message: 'Birthday date format is invalid.',
|
||||
code: 'BIRTHDAY_DATE_FORMAT_INVALID',
|
||||
id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -59,6 +65,8 @@ export const paramDef = {
|
|||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
|
||||
birthday: { type: 'string', nullable: true },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
|
@ -117,6 +125,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||
.innerJoinAndSelect('following.followee', 'followee');
|
||||
|
||||
if (ps.birthday) {
|
||||
try {
|
||||
const d = new Date(ps.birthday);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
||||
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
||||
birthdayUserQuery.select('user_profile.userId')
|
||||
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
|
||||
|
||||
query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`);
|
||||
} catch (err) {
|
||||
throw new ApiError(meta.errors.birthdayInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
const followings = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
|
|
@ -28,7 +28,7 @@ export const meta = {
|
|||
|
||||
errors: {
|
||||
unavailable: {
|
||||
message: 'Translate of notes unavailable.',
|
||||
message: 'Translate of description unavailable.',
|
||||
code: 'UNAVAILABLE',
|
||||
id: '50a70314-2d8a-431b-b433-efa5cc56444c',
|
||||
},
|
||||
|
|
|
@ -3113,6 +3113,10 @@ type UserLite = {
|
|||
url: string;
|
||||
angle?: number;
|
||||
flipH?: boolean;
|
||||
scale?: number;
|
||||
moveX?: number;
|
||||
moveY?: number;
|
||||
opacity?: number;
|
||||
}[];
|
||||
emojis: {
|
||||
name: string;
|
||||
|
@ -3138,8 +3142,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
|||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:665:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:117:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:638:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:121:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:642: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)
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"three": "0.158.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tinyld": "^1.3.4",
|
||||
"tsc-alias": "1.8.8",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
|
|
|
@ -216,20 +216,24 @@ export async function common(createVue: () => App<Element>) {
|
|||
}
|
||||
}, { immediate: true });
|
||||
|
||||
if (defaultStore.state.keepScreenOn) {
|
||||
if ('wakeLock' in navigator) {
|
||||
navigator.wakeLock.request('screen')
|
||||
.then(() => {
|
||||
document.addEventListener('visibilitychange', async () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
navigator.wakeLock.request('screen');
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// If Permission fails on an AppleDevice such as Safari
|
||||
});
|
||||
// Keep screen on
|
||||
const onVisibilityChange = () => document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
navigator.wakeLock.request('screen');
|
||||
}
|
||||
});
|
||||
if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) {
|
||||
navigator.wakeLock.request('screen')
|
||||
.then(onVisibilityChange)
|
||||
.catch(() => {
|
||||
// On WebKit-based browsers, user activation is required to send wake lock request
|
||||
// https://webkit.org/blog/13862/the-user-activation-api/
|
||||
document.addEventListener(
|
||||
'click',
|
||||
() => navigator.wakeLock.request('screen').then(onVisibilityChange),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//#region Fetch user
|
||||
|
|
|
@ -16,7 +16,22 @@ import MkButton from '@/components/MkButton.vue';
|
|||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
note: Misskey.entities.Note;
|
||||
text: string | null;
|
||||
files: Misskey.entities.DriveFile[];
|
||||
poll?: {
|
||||
expiresAt: string | null;
|
||||
multiple: boolean;
|
||||
choices: {
|
||||
isVoted: boolean;
|
||||
text: string;
|
||||
votes: number;
|
||||
}[];
|
||||
} | {
|
||||
choices: string[];
|
||||
multiple: boolean;
|
||||
expiresAt: string | null;
|
||||
expiredAfter: string | null;
|
||||
};
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -25,9 +40,9 @@ const emit = defineEmits<{
|
|||
|
||||
const label = computed(() => {
|
||||
return concat([
|
||||
props.note.text ? [i18n.t('_cw.chars', { count: props.note.text.length })] : [],
|
||||
props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length })] : [],
|
||||
props.note.poll != null ? [i18n.ts.poll] : [],
|
||||
props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [],
|
||||
props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [],
|
||||
props.poll != null ? [i18n.ts.poll] : [],
|
||||
] as string[][]).join(' / ');
|
||||
});
|
||||
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<button class="_button" :class="$style.root" @mousedown="toggle">
|
||||
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
|
||||
<span v-if="!modelValue" :class="$style.label">{{ label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { concat } from '@/scripts/array.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
note: Misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', v: boolean): void;
|
||||
}>();
|
||||
|
||||
const label = computed(() => {
|
||||
return concat([
|
||||
props.note.files && props.note.files.length !== 0 ? [i18n.t('withNFiles', { n: props.note.files.length })] : [],
|
||||
props.note.poll != null ? [i18n.ts.poll] : [],
|
||||
] as string[][]).join(' / ');
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
emit('update:modelValue', !props.modelValue);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: inline-block;
|
||||
margin: 5px 0;
|
||||
padding: 6px;
|
||||
font-size: 0.7em;
|
||||
color: var(--cwFg);
|
||||
background: var(--cwBg);
|
||||
border-radius: 5px;
|
||||
transition: background-color .25s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--cwBg);
|
||||
background: var(--panel);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 4px;
|
||||
|
||||
&:before {
|
||||
content: '(';
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ')';
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<input v-model="query" :class="$style.input" type="search" :placeholder="q">
|
||||
<button :class="$style.button" @click="search"><i class="ti ti-search"></i> {{ i18n.ts.searchByGoogle }}</button>
|
||||
<input v-model="query" :class="$style.input" type="search" :placeholder="q" @click.stop>
|
||||
<button :class="$style.button" @click.stop="search"><i class="ti ti-search"></i> {{ i18n.ts.searchByGoogle }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<component
|
||||
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target"
|
||||
:title="url"
|
||||
@click.stop
|
||||
>
|
||||
<slot></slot>
|
||||
<i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i>
|
||||
|
|
|
@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkAvatar v-if="!defaultStore.state.hideAvatarsInNote" :class="$style.collapsedRenoteTargetAvatar" :user="appearNote.user" link preview/>
|
||||
<Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'respect'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/>
|
||||
</div>
|
||||
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
|
||||
<article v-else :class="$style.article" :style="{ cursor: expandOnNoteClick ? 'pointer' : '' }" @click="noteClick" @contextmenu.stop="onContextmenu">
|
||||
<div style="display: flex; padding-bottom: 10px;">
|
||||
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
|
||||
<MkAvatar v-if="!defaultStore.state.hideAvatarsInNote" :class="[$style.avatar, { [$style.avatarReplyTo]: appearNote.reply, [$style.showEl]: !appearNote.reply && (showEl && ['hideHeaderOnly', 'hideHeaderFloatBtn', 'hide'].includes(<string>defaultStore.state.displayHeaderNavBarWhenScroll)) && mainRouter.currentRoute.value.name === 'index', [$style.showElTab]: !appearNote.reply && (showEl && ['hideHeaderOnly', 'hideHeaderFloatBtn', 'hide'].includes(<string>defaultStore.state.displayHeaderNavBarWhenScroll)) && mainRouter.currentRoute.value.name !== 'index' }]" :user="appearNote.user" :link="!mock" :preview="!mock"/>
|
||||
|
@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkEvent v-if="appearNote.event" :note="appearNote"/>
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" :text="appearNote.cw" :author="appearNote.user" :nyaize="noNyaize ? false : 'respect'"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;"/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
|
||||
<div :class="$style.text">
|
||||
|
@ -85,7 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && appearNote.text" style="padding-top: 5px; color: var(--accent);">
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && appearNote.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @mousedown="translate()">{{ i18n.ts.translateNote }}</button>
|
||||
<button v-else class="_button" @mousedown="translation = null">{{ i18n.ts.close }}</button>
|
||||
</div>
|
||||
|
@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}:</b><hr style="margin: 10px 0;">
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="false" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="noNyaize ? false : 'respect'" :emojiUrls="appearNote.emojis" @click.stop/>
|
||||
<div v-if="translation.translator == 'ctav3'" style="margin-top: 10px; padding: 0 0 15px;">
|
||||
<img v-if="!defaultStore.state.darkMode" src="/client-assets/color-short.svg" alt="" style="float: right;">
|
||||
<img v-else src="/client-assets/white-short.svg" alt="" style="float: right;"/>
|
||||
|
@ -107,18 +107,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files.length > 0">
|
||||
<MkMediaList v-if="appearNote.disableRightClick" :mediaList="appearNote.files" @contextmenu.prevent/>
|
||||
<MkMediaList v-else :mediaList="appearNote.files"/>
|
||||
<MkMediaList v-if="appearNote.disableRightClick" :mediaList="appearNote.files" @click.stop @contextmenu.prevent/>
|
||||
<MkMediaList v-else :mediaList="appearNote.files" @click.stop/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
|
||||
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/>
|
||||
<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) || (appearNote.files.length > 0 && defaultStore.state.allMediaNoteCollapse)) && collapsed" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
||||
<button v-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || (appearNote.files.length > 0 && defaultStore.state.allMediaNoteCollapse)) && collapsed" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :class="$style.collapsed" class="_button" @click.stop="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) || (appearNote.files.length > 0 && defaultStore.state.allMediaNoteCollapse)) && !collapsed" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :class="$style.showLess" class="_button" @click="collapsed = true">
|
||||
<button v-else-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || (appearNote.files.length > 0 && defaultStore.state.allMediaNoteCollapse)) && !collapsed" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :class="$style.showLess" class="_button" @click.stop="collapsed = true">
|
||||
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -126,13 +126,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<div>
|
||||
<MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
||||
<MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
|
||||
<template #more>
|
||||
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
|
||||
</template>
|
||||
</MkReactionsViewer>
|
||||
<footer :class="$style.footer">
|
||||
<button v-if="!note.isHidden" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.reply" :class="$style.footerButton" class="_button" @click="reply()">
|
||||
<button v-if="!note.isHidden" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.reply" :class="$style.footerButton" class="_button" @click.stop="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
|
@ -161,10 +161,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-if="appearNote.myReaction == null" v-tooltip="i18n.ts.reaction" class="ti ti-mood-plus"></i>
|
||||
<i v-else v-tooltip="i18n.ts.editReaction" class="ti ti-mood-edit"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null && appearNote.reactionAcceptance == 'likeOnly'" ref="reactButton" v-vibrate="defaultStore.state.vibrateSystem ? [30, 50, 50] : []" v-tooltip="i18n.ts.removeReaction" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
|
||||
<button v-if="appearNote.myReaction != null && appearNote.reactionAcceptance == 'likeOnly'" ref="reactButton" v-vibrate="defaultStore.state.vibrateSystem ? [30, 50, 50] : []" v-tooltip="i18n.ts.removeReaction" :class="$style.footerButton" class="_button" @click.stop="undoReact(appearNote)">
|
||||
<i class="ti ti-heart-minus"></i>
|
||||
</button>
|
||||
<button v-if="canRenote && defaultStore.state.renoteQuoteButtonSeparation" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.quote" class="_button" :class="$style.footerButton" @click="quote()">
|
||||
<button v-if="canRenote && defaultStore.state.renoteQuoteButtonSeparation" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.quote" class="_button" :class="$style.footerButton" @click.stop="quote()">
|
||||
<i class="ti ti-quote"></i>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.clip" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
||||
|
@ -233,12 +233,13 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
|||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { shouldCollapsed, shouldMfmCollapsed } from '@/scripts/collapsed.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { mainRouter, useRouter } 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';
|
||||
import { vibrate } from '@/scripts/vibrate.js';
|
||||
import detectLanguage from '@/scripts/detect-language.js';
|
||||
|
||||
let showEl = $ref(false);
|
||||
|
||||
|
@ -314,6 +315,8 @@ const translating = ref(false);
|
|||
const viewTextSource = ref(false);
|
||||
const noNyaize = ref(false);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
||||
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
|
||||
const router = useRouter();
|
||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||
|
||||
const collapseLabel = computed(() => {
|
||||
|
@ -388,6 +391,11 @@ if (!props.mock) {
|
|||
});
|
||||
}
|
||||
|
||||
function noteClick(ev: MouseEvent) {
|
||||
if (document.getSelection().type === 'Range' || !expandOnNoteClick) ev.stopPropagation();
|
||||
else router.push(notePage(appearNote));
|
||||
}
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
|
@ -592,6 +600,12 @@ async function clip() {
|
|||
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
}
|
||||
|
||||
const isForeignLanguage: boolean = appearNote.text != null && (() => {
|
||||
const targetLang = (miLocalStorage.getItem('lang') ?? navigator.language).slice(0, 2);
|
||||
const postLang = detectLanguage(appearNote.text);
|
||||
return postLang !== '' && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
|
|
|
@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkEvent v-if="appearNote.event" :note="appearNote"/>
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="noNyaize ? false : 'respect'"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote"/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts._ffVisibility.private }})</span>
|
||||
|
@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && appearNote.text" style="padding-top: 5px; color: var(--accent);">
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && appearNote.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @mousedown="translate()">{{ i18n.ts.translateNote }}</button>
|
||||
<button v-else class="_button" @mousedown="translation = null">{{ i18n.ts.close }}</button>
|
||||
</div>
|
||||
|
@ -113,7 +113,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}:</b><hr style="margin: 10px 0;">
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="false" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="noNyaize ? false : 'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
<div v-if="translation.translator == 'ctav3'" style="margin-top: 10px; padding: 0 0 15px;">
|
||||
<img v-if="!defaultStore.state.darkMode" src="/client-assets/color-short.svg" alt="" style="float: right;">
|
||||
<img v-else src="/client-assets/white-short.svg" alt="" style="float: right;"/>
|
||||
|
@ -315,6 +315,7 @@ import { infoImageUrl, instance } from '@/instance.js';
|
|||
import MkPostForm from '@/components/MkPostFormSimple.vue';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { vibrate } from '@/scripts/vibrate.js';
|
||||
import detectLanguage from '@/scripts/detect-language.js';
|
||||
|
||||
const MOBILE_THRESHOLD = 500;
|
||||
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
|
||||
|
@ -601,6 +602,12 @@ function menu(viaKeyboard = false): void {
|
|||
}).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
const isForeignLanguage: boolean = appearNote.text != null && (() => {
|
||||
const targetLang = (miLocalStorage.getItem('lang') ?? navigator.language).slice(0, 2);
|
||||
const postLang = detectLanguage(appearNote.text);
|
||||
return postLang !== '' && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
|
|
|
@ -11,7 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkUserName :user="user" :nowrap="true"/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<p v-if="useCw" :class="$style.cw">
|
||||
<Mfm v-if="cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
|
||||
<MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/>
|
||||
</p>
|
||||
<div v-show="!useCw || showContent">
|
||||
<Mfm :text="text.trim()" :author="user" :nyaize="'respect'" :i="user"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
</div>
|
||||
|
@ -21,15 +25,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import * as mfm from 'cherrypick-mfm-js';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const showContent = ref(false);
|
||||
|
||||
const props = defineProps<{
|
||||
text: string;
|
||||
files: Misskey.entities.DriveFile[];
|
||||
poll?: {
|
||||
choices: string[];
|
||||
multiple: boolean;
|
||||
expiresAt: string | null;
|
||||
expiredAfter: string | null;
|
||||
};
|
||||
useCw: boolean;
|
||||
cw: string | null;
|
||||
user: Misskey.entities.User;
|
||||
showProfile?: boolean;
|
||||
}>();
|
||||
|
@ -62,6 +78,14 @@ const urls = props.text ? extractUrlFromMfm(mfm.parse(props.text)) : null;
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.cw {
|
||||
cursor: default;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2px;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkEvent v-if="note.event" :note="note"/>
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
<MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
<MkSubNoteContent :class="$style.text" :note="note"/>
|
||||
|
|
|
@ -9,13 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.main">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
<MkAvatar v-if="!defaultStore.state.hideAvatarsInNote" :class="$style.avatar" :user="note.user" link preview/>
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.body" :style="{ cursor: expandOnNoteClick ? 'pointer' : '' }" @click="noteClick">
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div>
|
||||
<MkEvent v-if="note.event" :note="note"/>
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/>
|
||||
<MkCwButton v-model="showContent" style="width: 100%" :note="note"/>
|
||||
<MkCwButton v-model="showContent" style="width: 100%" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
<MkSubNoteContent :class="$style.text" :note="note" :showSubNoteFooterButton="defaultStore.state.showSubNoteFooterButton"/>
|
||||
|
@ -55,6 +55,7 @@ import { $i } from '@/account.js';
|
|||
import { userPage } from '@/filters/user.js';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
let hideLine = $ref(false);
|
||||
|
||||
|
@ -70,6 +71,9 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
|
||||
|
||||
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
|
||||
const router = useRouter();
|
||||
|
||||
let showContent = $ref(false);
|
||||
let replies: Misskey.entities.Note[] = $ref([]);
|
||||
|
||||
|
@ -82,6 +86,11 @@ if (props.detail) {
|
|||
hideLine = true;
|
||||
});
|
||||
}
|
||||
|
||||
function noteClick(ev: MouseEvent) {
|
||||
if (document.getSelection().type === 'Range' || !expandOnNoteClick) ev.stopPropagation();
|
||||
else router.push(notePage(props.note));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
|
||||
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated, watch } from 'vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import XNotification from '@/components/MkNotification.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
|
@ -44,7 +44,7 @@ const props = defineProps<{
|
|||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
|
||||
let pagination = $computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? {
|
||||
endpoint: 'i/notifications-grouped' as const,
|
||||
limit: 20,
|
||||
params: computed(() => ({
|
||||
|
@ -56,7 +56,7 @@ const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
|
|||
params: computed(() => ({
|
||||
excludeTypes: props.excludeTypes ?? undefined,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
function onNotification(notification) {
|
||||
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
|
||||
|
|
|
@ -186,7 +186,7 @@ watch([$$(backed), $$(contentEl)], () => {
|
|||
});
|
||||
|
||||
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
|
||||
watch(() => props.pagination.params, init, { deep: true });
|
||||
watch(() => [props.pagination.endpoint, props.pagination.params], init, { deep: true });
|
||||
|
||||
watch(queue, (a, b) => {
|
||||
if (a.size === 0 && b.size === 0) return;
|
||||
|
|
|
@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :user="postAccount ?? $i" :showProfile="showProfilePreview"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i" :showProfile="showProfilePreview"/>
|
||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||
</div>
|
||||
<footer :class="$style.footer">
|
||||
|
|
|
@ -88,7 +88,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<input v-show="withHashtags && showForm" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
<XPostFormAttaches v-if="showForm" v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||
<MkPollEditor v-if="poll && showForm" v-model="poll" @destroyed="poll = null"/>
|
||||
<MkNotePreview v-if="showPreview && showForm" :class="$style.preview" :text="text" :user="postAccount ?? $i" :showProfile="showProfilePreview"/>
|
||||
<MkNotePreview v-if="showPreview && showForm" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i" :showProfile="showProfilePreview"/>
|
||||
<div v-if="showingOptions && showForm" style="padding: 8px 16px;">
|
||||
</div>
|
||||
<Transition
|
||||
|
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && note.text" style="padding-top: 5px; color: var(--accent);">
|
||||
<div v-if="defaultStore.state.showTranslateButtonInNote && instance.translatorAvailable && $i && note.text && isForeignLanguage" style="padding-top: 5px; color: var(--accent);">
|
||||
<button v-if="!(translating || translation)" ref="translateButton" class="_button" @mousedown="translate()">{{ i18n.ts.translateNote }}</button>
|
||||
<button v-else class="_button" @mousedown="translation = null">{{ i18n.ts.close }}</button>
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}:</b><hr style="margin: 10px 0;">
|
||||
<Mfm :text="translation.text" :author="note.user" :nyaize="false" :emojiUrls="note.emojis"/>
|
||||
<Mfm :text="translation.text" :author="note.user" :nyaize="noNyaize ? false : 'respect'" :emojiUrls="note.emojis" @click.stop/>
|
||||
<div v-if="translation.translator == 'ctav3'" style="margin-top: 10px; padding: 0 0 15px;">
|
||||
<img v-if="!defaultStore.state.darkMode" src="/client-assets/color-short.svg" alt="" style="float: right;">
|
||||
<img v-else src="/client-assets/white-short.svg" alt="" style="float: right;"/>
|
||||
|
@ -42,31 +42,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div v-show="showContent">
|
||||
<div v-if="note.files.length > 0">
|
||||
<MkMediaList v-if="note.disableRightClick" :mediaList="note.files" @contextmenu.prevent/>
|
||||
<MkMediaList v-else :mediaList="note.files"/>
|
||||
<MkMediaList v-if="note.disableRightClick" :mediaList="note.files" @click.stop @contextmenu.prevent/>
|
||||
<MkMediaList v-else :mediaList="note.files" @click.stop/>
|
||||
</div>
|
||||
<div v-if="note.poll">
|
||||
<MkPoll :note="note"/>
|
||||
<MkPoll :note="note" @click.stop/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || note.files.length > 0 || note.poll) && collapsed" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :class="$style.fade" class="_button" @click="collapsed = false;">
|
||||
<button v-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || note.files.length > 0 || note.poll) && collapsed" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :class="$style.fade" class="_button" @click.stop="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) || note.files.length > 0 || note.poll) && !collapsed" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :class="$style.showLess" class="_button" @click="collapsed = true;">
|
||||
<button v-else-if="(isLong || (isMFM && defaultStore.state.collapseDefault) || note.files.length > 0 || note.poll) && !collapsed" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :class="$style.showLess" class="_button" @click.stop="collapsed = true;">
|
||||
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
|
||||
</button>
|
||||
<div v-if="showSubNoteFooterButton">
|
||||
<MkReactionsViewer v-show="note.cw == null || showContent" :note="note" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
||||
<MkReactionsViewer v-show="note.cw == null || showContent" :note="note" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
|
||||
<template #more>
|
||||
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
|
||||
</template>
|
||||
</MkReactionsViewer>
|
||||
<footer :class="$style.footer">
|
||||
<button v-if="!note.isHidden" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.reply" :class="$style.footerButton" class="_button" @click="reply()">
|
||||
<button v-if="!note.isHidden" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.reply" :class="$style.footerButton" class="_button" @click.stop="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="note.repliesCount > 0" :class="$style.footerButtonCount">{{ note.repliesCount }}</p>
|
||||
</button>
|
||||
|
@ -95,10 +95,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-if="note.myReaction == null" v-tooltip="i18n.ts.reaction" class="ti ti-mood-plus"></i>
|
||||
<i v-else v-tooltip="i18n.ts.editReaction" class="ti ti-mood-edit"></i>
|
||||
</button>
|
||||
<button v-if="note.myReaction != null && note.reactionAcceptance == 'likeOnly'" ref="reactButton" v-vibrate="defaultStore.state.vibrateSystem ? [30, 50, 50] : []" v-tooltip="i18n.ts.removeReaction" :class="$style.footerButton" class="_button" @click="undoReact(note)">
|
||||
<button v-if="note.myReaction != null && note.reactionAcceptance == 'likeOnly'" ref="reactButton" v-vibrate="defaultStore.state.vibrateSystem ? [30, 50, 50] : []" v-tooltip="i18n.ts.removeReaction" :class="$style.footerButton" class="_button" @click.stop="undoReact(note)">
|
||||
<i class="ti ti-heart-minus"></i>
|
||||
</button>
|
||||
<button v-if="canRenote && defaultStore.state.renoteQuoteButtonSeparation" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.quote" class="_button" :class="$style.footerButton" @click="quote()">
|
||||
<button v-if="canRenote && defaultStore.state.renoteQuoteButtonSeparation" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.quote" class="_button" :class="$style.footerButton" @click.stop="quote()">
|
||||
<i class="ti ti-quote"></i>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" v-tooltip="i18n.ts.clip" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
||||
|
@ -143,6 +143,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
|||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||
import { concat } from '@/scripts/array.js';
|
||||
import { vibrate } from '@/scripts/vibrate.js';
|
||||
import detectLanguage from '@/scripts/detect-language.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -403,6 +404,12 @@ async function clip() {
|
|||
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
}
|
||||
|
||||
const isForeignLanguage: boolean = note.text != null && (() => {
|
||||
const targetLang = (miLocalStorage.getItem('lang') ?? navigator.language).slice(0, 2);
|
||||
const postLang = detectLanguage(note.text);
|
||||
return postLang !== '' && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
|
|
|
@ -37,6 +37,7 @@ import { unisonReload } from '@/scripts/unison-reload.js';
|
|||
import * as os from '@/os.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
import { clearCache } from '@/scripts/clear-cache.js';
|
||||
|
||||
let showChangelog = $ref(false);
|
||||
|
||||
|
@ -66,19 +67,9 @@ const close = async () => {
|
|||
});
|
||||
return;
|
||||
}
|
||||
cacheClear();
|
||||
await clearCache();
|
||||
};
|
||||
|
||||
function cacheClear() {
|
||||
os.waiting();
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('theme');
|
||||
miLocalStorage.removeItem('emojis');
|
||||
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
||||
fetchCustomEmojis(true);
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
confetti({
|
||||
duration: 1000 * 3,
|
||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else>invalid url</span>
|
||||
</div>
|
||||
<div :class="$style.action">
|
||||
<MkButton :small="true" inline @click="playerEnabled = false">
|
||||
<MkButton :small="true" inline @click.stop="playerEnabled = false">
|
||||
<i class="ti ti-x"></i> {{ i18n.ts.disablePlayer }}
|
||||
</MkButton>
|
||||
</div>
|
||||
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
></iframe>
|
||||
</div>
|
||||
<div :class="$style.action">
|
||||
<MkButton :small="true" inline @click="tweetExpanded = false">
|
||||
<MkButton :small="true" inline @click.stop="tweetExpanded = false">
|
||||
<i class="ti ti-x"></i> {{ i18n.ts.close }}
|
||||
</MkButton>
|
||||
</div>
|
||||
|
@ -66,15 +66,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</component>
|
||||
<template v-if="showActions">
|
||||
<div v-if="tweetId" :class="$style.action">
|
||||
<MkButton :small="true" inline @click="tweetExpanded = true">
|
||||
<MkButton :small="true" inline @click.stop="tweetExpanded = true">
|
||||
<i class="ti ti-brand-x"></i> {{ i18n.ts.expandTweet }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="!playerEnabled && player.url" :class="$style.action">
|
||||
<MkButton :small="true" inline @click="playerEnabled = true">
|
||||
<MkButton :small="true" inline @click.stop="playerEnabled = true">
|
||||
<i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }}
|
||||
</MkButton>
|
||||
<MkButton v-if="!isMobile" :small="true" inline @click="openPlayer()">
|
||||
<MkButton v-if="!isMobile" :small="true" inline @click.stop="openPlayer()">
|
||||
<i class="ti ti-picture-in-picture"></i> {{ i18n.ts.openInWindow }}
|
||||
</MkButton>
|
||||
</div>
|
||||
|
|
|
@ -70,6 +70,7 @@ import { mainRouter } from '@/router.js';
|
|||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
|
||||
let showFollowButton = $ref(false);
|
||||
|
@ -88,12 +89,7 @@ type Tab = {
|
|||
const props = withDefaults(defineProps<{
|
||||
tabs?: Tab[];
|
||||
tab?: string;
|
||||
actions?: {
|
||||
text: string;
|
||||
icon: string;
|
||||
highlighted?: boolean;
|
||||
handler: (ev: MouseEvent) => void;
|
||||
}[];
|
||||
actions?: PageHeaderItem[];
|
||||
thin?: boolean;
|
||||
displayMyAvatar?: boolean;
|
||||
}>(), {
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<a v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
|
||||
<a v-vibrate="defaultStore.state.vibrateSystem ? 5 : []" :href="to" :class="active ? activeClass : null" @click.prevent.stop="nav" @contextmenu.prevent.stop="onContextmenu">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,7 @@ 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">
|
||||
<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.stop="onClick">
|
||||
<MkImgWithBlurhash
|
||||
:class="$style.inner"
|
||||
:src="url"
|
||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
decoding="async"
|
||||
@error="errored = true"
|
||||
@load="errored = false"
|
||||
@click="onClick"
|
||||
@click.stop="onClick"
|
||||
@mouseover="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''"
|
||||
@mouseout="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = false : ''"
|
||||
@touchstart="defaultStore.state.showingAnimatedImages === 'interaction' ? playAnimation = true : ''"
|
||||
|
|
|
@ -4,9 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<img v-if="!useOsNativeEmojis" :class="[$style.root, { [$style.large]: defaultStore.state.largeNoteReactions }]" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span>
|
||||
<span v-else>{{ emoji }}</span>
|
||||
<img v-if="!useOsNativeEmojis" :class="[$style.root, { [$style.large]: defaultStore.state.largeNoteReactions }]" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click.stop="onClick"/>
|
||||
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click.stop="onClick">{{ props.emoji }}</span>
|
||||
<span v-else @click.stop>{{ emoji }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
@ -104,7 +104,7 @@ export default function(props: MfmProps) {
|
|||
|
||||
case 'fn': {
|
||||
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
|
||||
let style;
|
||||
let style: string | undefined;
|
||||
switch (token.props.name) {
|
||||
case 'tada': {
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
|
@ -277,7 +277,7 @@ export default function(props: MfmProps) {
|
|||
]);
|
||||
}
|
||||
}
|
||||
if (style == null) {
|
||||
if (style === undefined) {
|
||||
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
|
||||
} else {
|
||||
return h('span', {
|
||||
|
|
|
@ -74,6 +74,7 @@ import { mainRouter } from '@/router.js';
|
|||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
|
||||
let showFollowButton = $ref(false);
|
||||
|
@ -84,12 +85,7 @@ const canBack = ref(['index', 'explore', 'my-notifications', 'messaging'].includ
|
|||
const props = withDefaults(defineProps<{
|
||||
tabs?: Tab[];
|
||||
tab?: string;
|
||||
actions?: {
|
||||
text: string;
|
||||
icon: string;
|
||||
highlighted?: boolean;
|
||||
handler: (ev: MouseEvent) => void;
|
||||
}[];
|
||||
actions?: PageHeaderItem[];
|
||||
thin?: boolean;
|
||||
displayMyAvatar?: boolean;
|
||||
}>(), {
|
||||
|
|
|
@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<component
|
||||
:is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel" :target="target"
|
||||
@click.stop
|
||||
@contextmenu.stop="() => {}"
|
||||
>
|
||||
<template v-if="!self">
|
||||
|
|
|
@ -39,6 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkTextarea v-model="hiddenTags">
|
||||
<template #label>{{ i18n.ts.hiddenTags }}</template>
|
||||
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
|
@ -72,6 +77,7 @@ import FormLink from '@/components/form/link.vue';
|
|||
let enableRegistration: boolean = $ref(false);
|
||||
let emailRequiredForSignup: boolean = $ref(false);
|
||||
let sensitiveWords: string = $ref('');
|
||||
let hiddenTags: string = $ref('');
|
||||
let preservedUsernames: string = $ref('');
|
||||
let tosUrl: string | null = $ref(null);
|
||||
let privacyPolicyUrl: string | null = $ref(null);
|
||||
|
@ -81,6 +87,7 @@ async function init() {
|
|||
enableRegistration = !meta.disableRegistration;
|
||||
emailRequiredForSignup = meta.emailRequiredForSignup;
|
||||
sensitiveWords = meta.sensitiveWords.join('\n');
|
||||
hiddenTags = meta.hiddenTags.join('\n');
|
||||
preservedUsernames = meta.preservedUsernames.join('\n');
|
||||
tosUrl = meta.tosUrl;
|
||||
privacyPolicyUrl = meta.privacyPolicyUrl;
|
||||
|
@ -93,6 +100,7 @@ function save() {
|
|||
tosUrl,
|
||||
privacyPolicyUrl,
|
||||
sensitiveWords: sensitiveWords.split('\n'),
|
||||
hiddenTags: hiddenTags.split('\n'),
|
||||
preservedUsernames: preservedUsernames.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
|
|
|
@ -86,6 +86,9 @@ import { defaultStore } from '@/store.js';
|
|||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -167,24 +170,40 @@ async function search() {
|
|||
|
||||
const headerActions = $computed(() => {
|
||||
if (channel && channel.userId) {
|
||||
const share = {
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
handler: async (): Promise<void> => {
|
||||
navigator.share({
|
||||
title: channel.name,
|
||||
text: channel.description,
|
||||
url: `${url}/channels/${channel.id}`,
|
||||
});
|
||||
},
|
||||
};
|
||||
const headerItems: PageHeaderItem[] = [];
|
||||
|
||||
const canEdit = ($i && $i.id === channel.userId) || iAmModerator;
|
||||
return canEdit ? [share, {
|
||||
icon: 'ti ti-settings',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}] : [share];
|
||||
headerItems.push({
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyUrl,
|
||||
handler: async (): Promise<void> => {
|
||||
copyToClipboard(`${url}/channels/${channel.id}`);
|
||||
os.success();
|
||||
},
|
||||
});
|
||||
|
||||
if (isSupportShare()) {
|
||||
headerItems.push({
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
handler: async (): Promise<void> => {
|
||||
navigator.share({
|
||||
title: channel.name,
|
||||
text: channel.description,
|
||||
url: `${url}/channels/${channel.id}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (($i && $i.id === channel.userId) || iAmModerator) {
|
||||
headerItems.push({
|
||||
icon: 'ti ti-settings',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
});
|
||||
}
|
||||
|
||||
return headerItems.length > 0 ? headerItems : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { url } from '@/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { clipsCache } from '@/cache';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
clipId: string,
|
||||
|
@ -118,6 +120,13 @@ const headerActions = $computed(() => clip && isOwned ? [{
|
|||
clipsCache.delete();
|
||||
},
|
||||
}, ...(clip.isPublic ? [{
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyUrl,
|
||||
handler: async (): Promise<void> => {
|
||||
copyToClipboard(`${url}/clips/${clip.id}`);
|
||||
os.success();
|
||||
},
|
||||
}] : []), ...(clip.isPublic && isSupportShare() ? [{
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
handler: async (): Promise<void> => {
|
||||
|
|
|
@ -46,7 +46,7 @@ function menu(ev) {
|
|||
os.apiGet('emoji', { name: props.emoji.name }).then(res => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: `License: ${res.license}`,
|
||||
text: `Name: ${res.name}\nAliases: ${res.aliases.join(' ')}\nCategory: ${res.category}\nisSensitive: ${res.isSensitive}\nlocalOnly: ${res.localOnly}\nLicense: ${res.license}\nURL: ${res.url}`,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -18,7 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
|
||||
<MkButton v-if="isSupportShare()" v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.ready">
|
||||
|
@ -70,6 +71,8 @@ import MkFolder from '@/components/MkFolder.vue';
|
|||
import MkCode from '@/components/MkCode.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
id: string;
|
||||
|
@ -89,6 +92,11 @@ function fetchFlash() {
|
|||
});
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
copyToClipboard(`${url}/play/${flash.id}`);
|
||||
os.success();
|
||||
}
|
||||
|
||||
function share() {
|
||||
navigator.share({
|
||||
title: flash.title,
|
||||
|
|
|
@ -29,7 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="other">
|
||||
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ti ti-pencil ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
|
||||
<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
|
@ -74,6 +75,8 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -102,6 +105,11 @@ function fetchPost() {
|
|||
});
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
copyToClipboard(`${url}/gallery/${post.id}`);
|
||||
os.success();
|
||||
}
|
||||
|
||||
function share() {
|
||||
navigator.share({
|
||||
title: post.title,
|
||||
|
|
|
@ -34,7 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div class="other">
|
||||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
|
||||
<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
|
@ -90,6 +91,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { pageViewInterruptors, defaultStore } from '@/store.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
pageName: string;
|
||||
|
@ -136,6 +139,11 @@ function share() {
|
|||
});
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
copyToClipboard(`${url}/@${page.user.username}/pages/${page.name}`);
|
||||
os.success();
|
||||
}
|
||||
|
||||
function shareWithNote() {
|
||||
os.post({
|
||||
initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`,
|
||||
|
|
|
@ -28,6 +28,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts._cherrypick.showRenoteConfirmPopup }}</template>
|
||||
<template #caption>{{ i18n.ts._cherrypick.showRenoteConfirmPopupDescription }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="expandOnNoteClick">
|
||||
<template #label>{{ i18n.ts._cherrypick.expandOnNoteClick }}</template>
|
||||
<template #caption>{{ i18n.ts._cherrypick.expandOnNoteClickDescription }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -96,10 +100,11 @@ const nicknameEnabled = computed(defaultStore.makeGetterSetter('nicknameEnabled'
|
|||
const useEnterToSend = computed(defaultStore.makeGetterSetter('useEnterToSend'));
|
||||
const postFormVisibilityHotkey = computed(defaultStore.makeGetterSetter('postFormVisibilityHotkey'));
|
||||
const showRenoteConfirmPopup = computed(defaultStore.makeGetterSetter('showRenoteConfirmPopup'));
|
||||
const expandOnNoteClick = computed(defaultStore.makeGetterSetter('expandOnNoteClick'));
|
||||
const displayHeaderNavBarWhenScroll = computed(defaultStore.makeGetterSetter('displayHeaderNavBarWhenScroll'));
|
||||
const reactableRemoteReactionEnabled = computed(defaultStore.makeGetterSetter('reactableRemoteReactionEnabled'));
|
||||
const showFollowingMessageInsteadOfButtonEnabled = computed(defaultStore.makeGetterSetter('showFollowingMessageInsteadOfButtonEnabled'));
|
||||
const mobileHeaderChange = computed(defaultStore.makeGetterSetter('mobileHeaderChange'));
|
||||
const displayHeaderNavBarWhenScroll = computed(defaultStore.makeGetterSetter('displayHeaderNavBarWhenScroll'));
|
||||
const renameTheButtonInPostFormToNya = computed(defaultStore.makeGetterSetter('renameTheButtonInPostFormToNya'));
|
||||
const friendlyEnableNotifications = computed(defaultStore.makeGetterSetter('friendlyEnableNotifications'));
|
||||
const friendlyEnableWidgets = computed(defaultStore.makeGetterSetter('friendlyEnableWidgets'));
|
||||
|
@ -113,6 +118,7 @@ watch([
|
|||
});
|
||||
|
||||
watch([
|
||||
expandOnNoteClick,
|
||||
reactableRemoteReactionEnabled,
|
||||
mobileHeaderChange,
|
||||
renameTheButtonInPostFormToNya,
|
||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<FormSection>
|
||||
<template #label>{{ i18n.ts.vibrations }} <span class="_beta">CherryPick</span></template>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="vibrate" @click="demoVibrate()">{{ i18n.ts.playVibrations }}<template #caption>{{ i18n.ts.playVibrationsDescription }}</template></MkSwitch>
|
||||
<MkSwitch v-model="vibrate" :disabled="ua" @click="demoVibrate()">{{ i18n.ts.playVibrations }}<template v-if="ua" #caption>{{ i18n.ts.cannotBeUsedFunc }} <a href="#" class="_link" @click="learnMorePlayVibrations">{{ i18n.ts.learnMore }}</a></template></MkSwitch>
|
||||
<MkSwitch v-if="vibrate" v-model="vibrateNote">{{ i18n.ts._vibrations.note }}</MkSwitch>
|
||||
<MkSwitch v-if="vibrate" v-model="vibrateNotification">{{ i18n.ts._vibrations.notification }}</MkSwitch>
|
||||
<MkSwitch v-if="vibrate" v-model="vibrateChat">{{ i18n.ts._vibrations.chat }}</MkSwitch>
|
||||
|
@ -62,6 +62,8 @@ import { operationTypes } from '@/scripts/sound.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
|
||||
const ua = /ipad|iphone/.test(navigator.userAgent.toLowerCase()) || !window.navigator.vibrate;
|
||||
|
||||
const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'));
|
||||
const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
|
||||
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
|
||||
|
@ -132,6 +134,13 @@ function demoVibrate() {
|
|||
window.navigator.vibrate(100);
|
||||
}
|
||||
|
||||
function learnMorePlayVibrations() {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts.playVibrationsDescription,
|
||||
});
|
||||
}
|
||||
|
||||
watch([
|
||||
vibrateSystem,
|
||||
], async () => {
|
||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:renote="renote"
|
||||
:initialVisibleUsers="visibleUsers"
|
||||
class="_panel"
|
||||
@posted="state = 'posted'"
|
||||
@posted="onPosted"
|
||||
/>
|
||||
<div v-else-if="state === 'posted'" class="_buttonsCenter">
|
||||
<MkButton primary @click="close">{{ i18n.ts.close }}</MkButton>
|
||||
|
@ -32,20 +32,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
// SPECIFICATION: https://misskey-hub.net/docs/features/share-form.html
|
||||
|
||||
import { } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { postMessageToParentWindow } from '@/scripts/post-message.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const localOnlyQuery = urlParams.get('localOnly');
|
||||
const visibilityQuery = urlParams.get('visibility') as typeof Misskey.noteVisibilities[number];
|
||||
|
||||
let state = $ref('fetching' as 'fetching' | 'writing' | 'posted');
|
||||
const state = ref<'fetching' | 'writing' | 'posted'>('fetching');
|
||||
let title = $ref(urlParams.get('title'));
|
||||
const text = urlParams.get('text');
|
||||
const url = urlParams.get('url');
|
||||
|
@ -144,7 +144,7 @@ async function init() {
|
|||
});
|
||||
}
|
||||
|
||||
state = 'writing';
|
||||
state.value = 'writing';
|
||||
}
|
||||
|
||||
init();
|
||||
|
@ -162,6 +162,11 @@ function goToMisskey(): void {
|
|||
location.href = '/';
|
||||
}
|
||||
|
||||
function onPosted(): void {
|
||||
state.value = 'posted';
|
||||
postMessageToParentWindow('misskey:shareForm:shareCompleted');
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
|
|
@ -80,7 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkOmit>
|
||||
<Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user"/>
|
||||
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
|
||||
<div v-if="user.description">
|
||||
<div v-if="user.description && isForeignLanguage">
|
||||
<MkButton v-if="!(translating || translation)" class="translateButton" small @click="translate"><i class="ti ti-language-hiragana"></i> {{ i18n.ts.translateProfile }}</MkButton>
|
||||
<MkButton v-else class="translateButton" small @click="translation = null"><i class="ti ti-x"></i> {{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
|
@ -187,6 +187,7 @@ import { defaultStore } from '@/store.js';
|
|||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { editNickname } from '@/scripts/edit-nickname.js';
|
||||
import { vibrate } from '@/scripts/vibrate.js';
|
||||
import detectLanguage from '@/scripts/detect-language.js';
|
||||
|
||||
function calcAge(birthdate: string): number {
|
||||
const date = new Date(birthdate);
|
||||
|
@ -290,6 +291,12 @@ async function updateMemo() {
|
|||
isEditingMemo = false;
|
||||
}
|
||||
|
||||
const isForeignLanguage: boolean = user.description != null && (() => {
|
||||
const targetLang = (miLocalStorage.getItem('lang') ?? navigator.language).slice(0, 2);
|
||||
const postLang = detectLanguage(user.description);
|
||||
return postLang !== '' && postLang !== targetLang;
|
||||
})();
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
if (translation.value != null) return;
|
||||
translating.value = true;
|
||||
|
|
13
packages/frontend/src/scripts/detect-language.ts
Normal file
13
packages/frontend/src/scripts/detect-language.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { detect } from 'tinyld';
|
||||
import * as mfm from 'cherrypick-mfm-js';
|
||||
|
||||
export default function detectLanguage(text: string): string {
|
||||
const nodes = mfm.parse(text);
|
||||
const filtered = mfm.extract(nodes, (node) => {
|
||||
return node.type === 'text' || node.type === 'quote';
|
||||
});
|
||||
const purified = mfm.toString(filtered);
|
||||
|
||||
if (detect(purified) === '') return 'en';
|
||||
return detect(purified);
|
||||
}
|
|
@ -18,6 +18,7 @@ import { getUserMenu } from '@/scripts/get-user-menu.js';
|
|||
import { clipsCache } from '@/cache.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
|
||||
export async function getNoteClipMenu(props: {
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -339,11 +340,12 @@ export function getNoteMenu(props: {
|
|||
danger: true,
|
||||
action: unclip,
|
||||
}, null] : []
|
||||
), {
|
||||
), ...(isSupportShare() ? [{
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
action: share,
|
||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
|
||||
}] : []),
|
||||
getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
|
||||
, {
|
||||
icon: 'ti ti-copy',
|
||||
text: i18n.ts.copyContent,
|
||||
|
|
8
packages/frontend/src/scripts/navigator.ts
Normal file
8
packages/frontend/src/scripts/navigator.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function isSupportShare(): boolean {
|
||||
return 'share' in navigator;
|
||||
}
|
25
packages/frontend/src/scripts/post-message.ts
Normal file
25
packages/frontend/src/scripts/post-message.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const postMessageEventTypes = [
|
||||
'misskey:shareForm:shareCompleted',
|
||||
] as const;
|
||||
|
||||
export type PostMessageEventType = typeof postMessageEventTypes[number];
|
||||
|
||||
export type MiPostMessageEvent = {
|
||||
type: PostMessageEventType;
|
||||
payload?: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* 親フレームにイベントを送信
|
||||
*/
|
||||
export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
|
||||
window.postMessage({
|
||||
type,
|
||||
payload,
|
||||
}, '*');
|
||||
}
|
|
@ -164,7 +164,7 @@ export function play(operationType: OperationType) {
|
|||
if (sound.type == null || !canPlay) return;
|
||||
|
||||
canPlay = false;
|
||||
playFile(sound).then(() => {
|
||||
playFile(sound).finally(() => {
|
||||
// ごく短時間に音が重複しないように
|
||||
setTimeout(() => {
|
||||
canPlay = true;
|
||||
|
|
|
@ -574,7 +574,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
// - Settings/Sounds & Vibrations
|
||||
vibrate: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
default: !/ipad|iphone/.test(navigator.userAgent.toLowerCase()) && window.navigator.vibrate,
|
||||
},
|
||||
vibrateNote: {
|
||||
where: 'device',
|
||||
|
@ -618,6 +618,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
expandOnNoteClick: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
displayHeaderNavBarWhenScroll: {
|
||||
where: 'device',
|
||||
default: 'hideHeaderFloatBtn' as 'all' | 'hideHeaderOnly' | 'hideHeaderFloatBtn' | 'hideFloatBtnOnly' | 'hideFloatBtnNavBar' | 'hide',
|
||||
|
|
11
packages/frontend/src/types/page-header.ts
Normal file
11
packages/frontend/src/types/page-header.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export type PageHeaderItem = {
|
||||
text: string;
|
||||
icon: string;
|
||||
highlighted?: boolean;
|
||||
handler: (ev: MouseEvent) => void;
|
||||
};
|
127
packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
Normal file
127
packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
Normal file
|
@ -0,0 +1,127 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings">
|
||||
<template #icon><i class="ti ti-cake"></i></template>
|
||||
<template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
|
||||
|
||||
<div :class="$style.bdayFRoot">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else-if="users.length > 0" :class="$style.bdayFGrid">
|
||||
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
|
||||
</div>
|
||||
<div v-else :class="$style.bdayFFallback">
|
||||
<img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||
import { GetFormResultType } from '@/scripts/form.js';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const name = i18n.ts._widgets.birthdayFollowings;
|
||||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||
|
||||
const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||
widgetPropsDef,
|
||||
props,
|
||||
emit,
|
||||
);
|
||||
|
||||
const users = ref<Misskey.entities.FollowingFolloweePopulated[]>([]);
|
||||
const fetching = ref(true);
|
||||
let lastFetchedAt = '1970-01-01';
|
||||
|
||||
const fetch = () => {
|
||||
if (!$i) {
|
||||
users.value = [];
|
||||
fetching.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const lfAtD = new Date(lastFetchedAt);
|
||||
lfAtD.setHours(0, 0, 0, 0);
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
if (now > lfAtD) {
|
||||
os.api('users/following', {
|
||||
limit: 18,
|
||||
birthday: now.toISOString(),
|
||||
userId: $i.id,
|
||||
}).then(res => {
|
||||
users.value = res;
|
||||
fetching.value = false;
|
||||
});
|
||||
|
||||
lastFetchedAt = now.toISOString();
|
||||
}
|
||||
};
|
||||
|
||||
useInterval(fetch, 1000 * 60, {
|
||||
immediate: true,
|
||||
afterMounted: true,
|
||||
});
|
||||
|
||||
defineExpose<WidgetComponentExpose>({
|
||||
name,
|
||||
configure,
|
||||
id: props.widget ? props.widget.id : null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.bdayFRoot {
|
||||
overflow: hidden;
|
||||
min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2));
|
||||
}
|
||||
.bdayFGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 42px);
|
||||
grid-template-rows: repeat(3, 42px);
|
||||
place-content: center;
|
||||
gap: 8px;
|
||||
margin: var(--margin) auto;
|
||||
}
|
||||
|
||||
.bdayFFallback {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bdayFFallbackImage {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
max-width: 90%;
|
||||
margin-bottom: 8px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
</style>
|
|
@ -33,6 +33,7 @@ export default function(app: App) {
|
|||
app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue')));
|
||||
app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue')));
|
||||
app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue')));
|
||||
app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue')));
|
||||
}
|
||||
|
||||
export const widgets = [
|
||||
|
@ -63,4 +64,5 @@ export const widgets = [
|
|||
'aichan',
|
||||
'userList',
|
||||
'clicker',
|
||||
'birthdayFollowings',
|
||||
];
|
||||
|
|
|
@ -152,10 +152,14 @@ export function getConfig(): UserConfig {
|
|||
test: {
|
||||
environment: 'happy-dom',
|
||||
deps: {
|
||||
inline: [
|
||||
// XXX: misskey-dev/browser-image-resizer has no "type": "module"
|
||||
'browser-image-resizer',
|
||||
],
|
||||
optimizer: {
|
||||
web: {
|
||||
include: [
|
||||
// XXX: misskey-dev/browser-image-resizer has no "type": "module"
|
||||
'browser-image-resizer',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -878,6 +878,9 @@ importers:
|
|||
tinycolor2:
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
tinyld:
|
||||
specifier: ^1.3.4
|
||||
version: 1.3.4
|
||||
tsc-alias:
|
||||
specifier: 1.8.8
|
||||
version: 1.8.8
|
||||
|
@ -962,10 +965,10 @@ importers:
|
|||
version: 7.5.3
|
||||
'@storybook/vue3':
|
||||
specifier: 7.5.3
|
||||
version: 7.5.3(@vue/compiler-core@3.3.8)(vue@3.3.9)
|
||||
version: 7.5.3(@vue/compiler-core@3.3.9)(vue@3.3.9)
|
||||
'@storybook/vue3-vite':
|
||||
specifier: 7.5.3
|
||||
version: 7.5.3(@vue/compiler-core@3.3.8)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(vite@5.0.2)(vue@3.3.9)
|
||||
version: 7.5.3(@vue/compiler-core@3.3.9)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(vite@5.0.2)(vue@3.3.9)
|
||||
'@testing-library/vue':
|
||||
specifier: 8.0.1
|
||||
version: 8.0.1(@vue/compiler-sfc@3.3.9)(vue@3.3.9)
|
||||
|
@ -7158,7 +7161,7 @@ packages:
|
|||
file-system-cache: 2.3.0
|
||||
dev: true
|
||||
|
||||
/@storybook/vue3-vite@7.5.3(@vue/compiler-core@3.3.8)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(vite@5.0.2)(vue@3.3.9):
|
||||
/@storybook/vue3-vite@7.5.3(@vue/compiler-core@3.3.9)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(vite@5.0.2)(vue@3.3.9):
|
||||
resolution: {integrity: sha512-gkNwDDn2AKthAtaoPrHb0+2gi33UluxpfSq/M5COoMEVFphj6y/jyDa+OEYlceXgnD8g2xvX4/yv2TbTNDzmcQ==}
|
||||
engines: {node: ^14.18 || >=16}
|
||||
peerDependencies:
|
||||
|
@ -7168,7 +7171,7 @@ packages:
|
|||
dependencies:
|
||||
'@storybook/builder-vite': 7.5.3(typescript@5.3.2)(vite@5.0.2)
|
||||
'@storybook/core-server': 7.5.3
|
||||
'@storybook/vue3': 7.5.3(@vue/compiler-core@3.3.8)(vue@3.3.9)
|
||||
'@storybook/vue3': 7.5.3(@vue/compiler-core@3.3.9)(vue@3.3.9)
|
||||
'@vitejs/plugin-vue': 4.5.0(vite@5.0.2)(vue@3.3.9)
|
||||
magic-string: 0.30.5
|
||||
react: 18.2.0
|
||||
|
@ -7187,7 +7190,7 @@ packages:
|
|||
- vue
|
||||
dev: true
|
||||
|
||||
/@storybook/vue3@7.5.3(@vue/compiler-core@3.3.8)(vue@3.3.9):
|
||||
/@storybook/vue3@7.5.3(@vue/compiler-core@3.3.9)(vue@3.3.9):
|
||||
resolution: {integrity: sha512-JaxtOl3UD9YhPrOqHuKtpqHMnFril3sBUxx/no2yM/mZYmNpAVd/C6PFM839WCay1mAywPuUoebJvmwWxWijkw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
peerDependencies:
|
||||
|
@ -7199,7 +7202,7 @@ packages:
|
|||
'@storybook/global': 5.0.0
|
||||
'@storybook/preview-api': 7.5.3
|
||||
'@storybook/types': 7.5.3
|
||||
'@vue/compiler-core': 3.3.8
|
||||
'@vue/compiler-core': 3.3.9
|
||||
lodash: 4.17.21
|
||||
ts-dedent: 2.2.0
|
||||
type-fest: 2.19.0
|
||||
|
@ -10134,7 +10137,7 @@ packages:
|
|||
normalize-path: 3.0.0
|
||||
readdirp: 3.6.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
/chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
@ -12454,13 +12457,6 @@ packages:
|
|||
/fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
/fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
|
@ -14104,7 +14100,7 @@ packages:
|
|||
micromatch: 4.0.5
|
||||
walker: 1.0.8
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/jest-leak-detector@29.7.0:
|
||||
|
@ -18066,7 +18062,7 @@ packages:
|
|||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/rollup@4.6.0:
|
||||
|
@ -18086,7 +18082,7 @@ packages:
|
|||
'@rollup/rollup-win32-arm64-msvc': 4.6.0
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.6.0
|
||||
'@rollup/rollup-win32-x64-msvc': 4.6.0
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
/rrweb-cssom@0.6.0:
|
||||
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
|
||||
|
@ -19262,6 +19258,12 @@ packages:
|
|||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||
dev: false
|
||||
|
||||
/tinyld@1.3.4:
|
||||
resolution: {integrity: sha512-u26CNoaInA4XpDU+8s/6Cq8xHc2T5M4fXB3ICfXPokUQoLzmPgSZU02TAkFwFMJCWTjk53gtkS8pETTreZwCqw==}
|
||||
engines: {node: '>= 12.10.0', npm: '>= 6.12.0', yarn: '>= 1.20.0'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/tinypool@0.7.0:
|
||||
resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
|
@ -91,16 +91,10 @@ async function build() {
|
|||
await build();
|
||||
|
||||
if (process.argv.includes("--watch")) {
|
||||
const watcher = fs.watch('./', { recursive: true });
|
||||
|
||||
const watcher = fs.watch('./locales');
|
||||
for await (const event of watcher) {
|
||||
const filename = event.filename?.replaceAll('\\', '/');
|
||||
|
||||
if (/^packages\/[a-z]+\/src/.test(filename)) {
|
||||
await build();
|
||||
}
|
||||
|
||||
if (/^locales\/[a-z]+-[A-Z]+\.yml/.test(filename)) {
|
||||
if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) {
|
||||
locales = buildLocales();
|
||||
await copyFrontendLocales()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue