Merge branch 'develop' into refine-fanout-timeline
This commit is contained in:
commit
ae684e6076
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
sudo dpkg -i dockle.deb
|
||||
- run: |
|
||||
cp .config/docker_example.env .config/docker.env
|
||||
cp ./docker-compose.yml.example ./docker-compose.yml
|
||||
cp ./docker-compose_example.yml ./docker-compose.yml
|
||||
- run: |
|
||||
docker compose up -d web
|
||||
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
|
||||
|
|
|
|||
|
|
@ -22,16 +22,13 @@ jobs:
|
|||
api-json-name: [api-base.json, api-head.json]
|
||||
include:
|
||||
- api-json-name: api-base.json
|
||||
repo-name: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
ref: ${{ github.base_ref }}
|
||||
- api-json-name: api-head.json
|
||||
repo-name: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: ${{ matrix.repo-name }}
|
||||
ref: ${{ matrix.ref }}
|
||||
submodules: true
|
||||
- name: Install pnpm
|
||||
|
|
|
|||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -21,15 +21,24 @@
|
|||
- 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
|
||||
- Fix: 長い名前のチャンネルにおける投稿フォームの表示が崩れる問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
|
||||
|
|
@ -168,6 +177,7 @@
|
|||
### Client
|
||||
- Enhance: TLの返信表示オプションを記憶するように
|
||||
- Enhance: 投稿されてから時間が経過しているノートであることを視覚的に分かりやすく
|
||||
- Feat: 絵文字ピッカーのカテゴリに「/」を入れることでフォルダ分け表示できるように
|
||||
|
||||
### Server
|
||||
- Enhance: タイムライン取得時のパフォーマンスを向上
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ RUN apt-get update \
|
|||
&& corepack enable \
|
||||
&& groupadd -g "${GID}" misskey \
|
||||
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
|
||||
&& 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
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,18 @@ export default function generateDTS() {
|
|||
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
||||
),
|
||||
),
|
||||
ts.factory.createFunctionDeclaration(
|
||||
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
|
||||
undefined,
|
||||
ts.factory.createIdentifier('build'),
|
||||
undefined,
|
||||
[],
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
),
|
||||
undefined,
|
||||
),
|
||||
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
||||
];
|
||||
const printed = ts.createPrinter({
|
||||
|
|
|
|||
|
|
@ -314,6 +314,7 @@ export interface Locale {
|
|||
"createFolder": string;
|
||||
"renameFolder": string;
|
||||
"deleteFolder": string;
|
||||
"folder": string;
|
||||
"addFile": string;
|
||||
"emptyDrive": string;
|
||||
"emptyFolder": string;
|
||||
|
|
@ -440,7 +441,6 @@ export interface Locale {
|
|||
"notFound": string;
|
||||
"notFoundDescription": string;
|
||||
"uploadFolder": string;
|
||||
"cacheClear": string;
|
||||
"markAsReadAllNotifications": string;
|
||||
"markAsReadAllUnreadNotes": string;
|
||||
"markAsReadAllTalkMessages": string;
|
||||
|
|
@ -1030,6 +1030,8 @@ export interface Locale {
|
|||
"sensitiveWords": string;
|
||||
"sensitiveWordsDescription": string;
|
||||
"sensitiveWordsDescription2": string;
|
||||
"hiddenTags": string;
|
||||
"hiddenTagsDescription": string;
|
||||
"notesSearchNotAvailable": string;
|
||||
"license": string;
|
||||
"unfavoriteConfirm": string;
|
||||
|
|
@ -2108,6 +2110,7 @@ export interface Locale {
|
|||
"chooseList": string;
|
||||
};
|
||||
"clicker": string;
|
||||
"birthdayFollowings": string;
|
||||
};
|
||||
"_cw": {
|
||||
"hide": string;
|
||||
|
|
@ -2503,4 +2506,5 @@ export interface Locale {
|
|||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
};
|
||||
export function build(): Locale;
|
||||
export default locales;
|
||||
|
|
|
|||
|
|
@ -51,33 +51,37 @@ const primaries = {
|
|||
// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
|
||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
||||
export function build() {
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
||||
|
||||
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
||||
const removeEmpty = (obj) => {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (v === '') {
|
||||
delete obj[k];
|
||||
} else if (typeof v === 'object') {
|
||||
removeEmpty(v);
|
||||
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
||||
const removeEmpty = (obj) => {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (v === '') {
|
||||
delete obj[k];
|
||||
} else if (typeof v === 'object') {
|
||||
removeEmpty(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
removeEmpty(locales);
|
||||
return obj;
|
||||
};
|
||||
removeEmpty(locales);
|
||||
|
||||
export default Object.entries(locales)
|
||||
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||
const [lang] = k.split('-');
|
||||
switch (k) {
|
||||
case 'ja-JP': return v;
|
||||
case 'ja-KS':
|
||||
case 'en-US': return merge(locales['ja-JP'], v);
|
||||
default: return merge(
|
||||
locales['ja-JP'],
|
||||
locales['en-US'],
|
||||
locales[`${lang}-${primaries[lang]}`] ?? {},
|
||||
v
|
||||
);
|
||||
}
|
||||
})(), a), {});
|
||||
return Object.entries(locales)
|
||||
.reduce((a, [k, v]) => (a[k] = (() => {
|
||||
const [lang] = k.split('-');
|
||||
switch (k) {
|
||||
case 'ja-JP': return v;
|
||||
case 'ja-KS':
|
||||
case 'en-US': return merge(locales['ja-JP'], v);
|
||||
default: return merge(
|
||||
locales['ja-JP'],
|
||||
locales['en-US'],
|
||||
locales[`${lang}-${primaries[lang]}`] ?? {},
|
||||
v
|
||||
);
|
||||
}
|
||||
})(), a), {});
|
||||
}
|
||||
|
||||
export default build();
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ folderName: "フォルダー名"
|
|||
createFolder: "フォルダーを作成"
|
||||
renameFolder: "フォルダー名を変更"
|
||||
deleteFolder: "フォルダーを削除"
|
||||
folder: "フォルダー"
|
||||
addFile: "ファイルを追加"
|
||||
emptyDrive: "ドライブは空です"
|
||||
emptyFolder: "フォルダーは空です"
|
||||
|
|
@ -437,7 +438,6 @@ share: "共有"
|
|||
notFound: "見つかりません"
|
||||
notFoundDescription: "指定されたURLに該当するページはありませんでした。"
|
||||
uploadFolder: "既定アップロード先"
|
||||
cacheClear: "キャッシュを削除"
|
||||
markAsReadAllNotifications: "すべての通知を既読にする"
|
||||
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
||||
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
|
||||
|
|
@ -1027,6 +1027,8 @@ resetPasswordConfirm: "パスワードリセットしますか?"
|
|||
sensitiveWords: "センシティブワード"
|
||||
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
||||
hiddenTags: "非表示ハッシュタグ"
|
||||
hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。"
|
||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||
license: "ライセンス"
|
||||
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||
|
|
@ -2012,6 +2014,7 @@ _widgets:
|
|||
_userList:
|
||||
chooseList: "リストを選択"
|
||||
clicker: "クリッカー"
|
||||
birthdayFollowings: "今日誕生日のユーザー"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey 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"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -304,8 +304,6 @@ export class UserFollowingService implements OnModuleInit {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
||||
}
|
||||
|
||||
// Publish followed event
|
||||
|
|
@ -373,8 +371,6 @@ export class UserFollowingService implements OnModuleInit {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`);
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -186,6 +186,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;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ export const paramDef = {
|
|||
withFiles: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
excludeNsfw: { type: 'boolean', default: false },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ export const paramDef = {
|
|||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
excludeNsfw: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|||
api("users/notes", {
|
||||
userId: props.user.id,
|
||||
fileType: image,
|
||||
excludeNsfw: defaultStore.state.nsfw !== "ignore",
|
||||
limit: 10
|
||||
}).then((notes) => {
|
||||
for (const note of notes) {
|
||||
|
|
@ -198,7 +197,6 @@ const _sfc_main = defineComponent({
|
|||
api("users/notes", {
|
||||
userId: props.user.id,
|
||||
fileType: image,
|
||||
excludeNsfw: defaultStore.state.nsfw !== "ignore",
|
||||
limit: 10
|
||||
}).then(notes => {
|
||||
for (const note of notes) {
|
||||
|
|
|
|||
|
|
@ -202,20 +202,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(' / ');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
|
||||
<section>
|
||||
<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
|
||||
<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
||||
<header class="_acrylic" @click="shown = !shown">
|
||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> ({{ emojis.length }})
|
||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }})
|
||||
</header>
|
||||
<div v-if="shown" class="body">
|
||||
<button
|
||||
|
|
@ -23,15 +24,52 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
|
||||
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
||||
<header class="_acrylic" @click="shown = !shown">
|
||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
|
||||
</header>
|
||||
<div v-if="shown" style="padding-left: 9px;">
|
||||
<MkEmojiPickerSection
|
||||
v-for="child in customEmojiTree"
|
||||
:key="`custom:${child.value}`"
|
||||
:initialShown="initialShown"
|
||||
:emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
|
||||
:hasChildSection="child.children.length !== 0"
|
||||
:customEmojiTree="child.children"
|
||||
@chosen="nestedChosen"
|
||||
>
|
||||
{{ child.value || i18n.ts.other }}
|
||||
</MkEmojiPickerSection>
|
||||
</div>
|
||||
<div v-if="shown" class="body">
|
||||
<button
|
||||
v-for="emoji in emojis"
|
||||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, Ref } from 'vue';
|
||||
import { getEmojiName } from '@/scripts/emojilist.js';
|
||||
import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import { i18n } from '../i18n.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
emojis: string[] | Ref<string[]>;
|
||||
initialShown?: boolean;
|
||||
hasChildSection?: boolean;
|
||||
customEmojiTree?: CustomEmojiFolderTree[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -49,4 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
|||
elm.title = getEmojiName(emoji) ?? emoji;
|
||||
}
|
||||
|
||||
function nestedChosen(emoji: any, ev?: MouseEvent) {
|
||||
emit('chosen', emoji, ev);
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -73,18 +73,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-once class="group">
|
||||
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
|
||||
<XSection
|
||||
v-for="category in customEmojiCategories"
|
||||
:key="`custom:${category}`"
|
||||
v-for="child in customEmojiFolderRoot.children"
|
||||
:key="`custom:${child.value}`"
|
||||
:initialShown="false"
|
||||
:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
|
||||
:emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))"
|
||||
:hasChildSection="child.children.length !== 0"
|
||||
:customEmojiTree="child.children"
|
||||
@chosen="chosen"
|
||||
>
|
||||
{{ category || i18n.ts.other }}
|
||||
{{ child.value || i18n.ts.other }}
|
||||
</XSection>
|
||||
</div>
|
||||
<div v-once class="group">
|
||||
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
|
||||
<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" @chosen="chosen">{{ category }}</XSection>
|
||||
<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
|
|
@ -100,7 +102,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||
import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import {
|
||||
emojilist,
|
||||
emojiCharByCategory,
|
||||
UnicodeEmojiDef,
|
||||
unicodeEmojiCategories as categories,
|
||||
getEmojiName,
|
||||
CustomEmojiFolderTree
|
||||
} from '@/scripts/emojilist.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
|
|
@ -144,6 +153,35 @@ const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
|
|||
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
||||
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
||||
|
||||
const customEmojiFolderRoot: CustomEmojiFolderTree = { value: "", category: "", children: [] };
|
||||
|
||||
function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
|
||||
const parts = input.split('/').map(p => p.trim());
|
||||
let currentNode: CustomEmojiFolderTree = root;
|
||||
|
||||
for (const part of parts) {
|
||||
let existingNode = currentNode.children.find((node) => node.value === part);
|
||||
|
||||
if (!existingNode) {
|
||||
const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
|
||||
currentNode.children.push(newNode);
|
||||
existingNode = newNode;
|
||||
}
|
||||
|
||||
currentNode = existingNode;
|
||||
}
|
||||
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
customEmojiCategories.value.forEach(ec => {
|
||||
if (ec !== null) {
|
||||
parseAndMergeCategories(ec, customEmojiFolderRoot);
|
||||
}
|
||||
});
|
||||
|
||||
parseAndMergeCategories('', customEmojiFolderRoot);
|
||||
|
||||
watch(q, () => {
|
||||
if (emojisEl.value) emojisEl.value.scrollTop = 0;
|
||||
|
||||
|
|
@ -572,8 +610,7 @@ defineExpose({
|
|||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
line-height: 28px;
|
||||
z-index: 1;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ function close() {
|
|||
vertical-align: bottom;
|
||||
height: 100px;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div style="container-type: inline-size;">
|
||||
<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="'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;"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
|
||||
<div :class="$style.text">
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.noteContent">
|
||||
<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="'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.private }})</span>
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -20,11 +24,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
|
||||
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;
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -53,6 +69,14 @@ const props = defineProps<{
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.cw {
|
||||
cursor: default;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2px;
|
||||
font-weight: bold;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>
|
||||
<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"/>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>
|
||||
<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" :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"/>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -43,7 +43,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(() => ({
|
||||
|
|
@ -55,7 +55,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;
|
||||
|
|
|
|||
|
|
@ -73,7 +73,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"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||
</div>
|
||||
<footer :class="$style.footer">
|
||||
|
|
@ -366,8 +366,8 @@ function checkMissingMention() {
|
|||
return;
|
||||
}
|
||||
}
|
||||
hasNotSpecifiedMentions = false;
|
||||
}
|
||||
hasNotSpecifiedMentions = false;
|
||||
}
|
||||
|
||||
function addMissingMention() {
|
||||
|
|
@ -1059,8 +1059,9 @@ defineExpose({
|
|||
|
||||
.visibility {
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 210px;
|
||||
|
||||
&:enabled {
|
||||
> .headerRightButtonText {
|
||||
|
|
|
|||
|
|
@ -103,7 +103,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';
|
||||
|
|
@ -268,7 +268,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', {
|
||||
|
|
|
|||
|
|
@ -48,16 +48,12 @@ import { scrollToTop } from '@/scripts/scroll.js';
|
|||
import { globalEvents } from '@/events.js';
|
||||
import { injectPageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
|
||||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
|
||||
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;
|
||||
}>(), {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { ui } from '@/config.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { clearCache } from './scripts/clear-cache.js';
|
||||
|
||||
export const navbarItemDef = reactive({
|
||||
notifications: {
|
||||
|
|
@ -171,4 +172,11 @@ export const navbarItemDef = reactive({
|
|||
show: computed(() => $i != null),
|
||||
to: `/@${$i?.username}`,
|
||||
},
|
||||
cacheClear: {
|
||||
title: i18n.ts.clearCache,
|
||||
icon: 'ti ti-trash',
|
||||
action: (ev) => {
|
||||
clearCache();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -33,13 +33,11 @@ import { i18n } from '@/i18n.js';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
||||
import { signout, $i } from '@/account.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { clearCache } from '@/scripts/clear-cache.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import * as os from '@/os.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
|
||||
const indexInfo = {
|
||||
title: i18n.ts.settings,
|
||||
|
|
@ -182,13 +180,7 @@ const menuDef = computed(() => [{
|
|||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.clearCache,
|
||||
action: async () => {
|
||||
os.waiting();
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('theme');
|
||||
miLocalStorage.removeItem('emojis');
|
||||
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
||||
await fetchCustomEmojis(true);
|
||||
unisonReload();
|
||||
await clearCache();
|
||||
},
|
||||
}, {
|
||||
type: 'button',
|
||||
|
|
|
|||
|
|
@ -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 'misskey-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(() => []);
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ onMounted(() => {
|
|||
os.api('users/notes', {
|
||||
userId: props.user.id,
|
||||
withFiles: true,
|
||||
excludeNsfw: defaultStore.state.nsfw !== 'ignore',
|
||||
limit: 15,
|
||||
}).then(notes => {
|
||||
for (const note of notes) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
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';
|
||||
|
||||
export async function clearCache() {
|
||||
os.waiting();
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('theme');
|
||||
miLocalStorage.removeItem('emojis');
|
||||
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
||||
await fetchCustomEmojis(true);
|
||||
unisonReload();
|
||||
}
|
||||
|
|
@ -43,3 +43,9 @@ export function getEmojiName(char: string): string | null {
|
|||
return emojilist[idx].name;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CustomEmojiFolderTree {
|
||||
value: string;
|
||||
category: string;
|
||||
children: CustomEmojiFolderTree[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -280,11 +281,11 @@ export function getNoteMenu(props: {
|
|||
window.open(appearNote.url ?? appearNote.uri, '_blank');
|
||||
},
|
||||
} : undefined,
|
||||
{
|
||||
...(isSupportShare() ? [{
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
action: share,
|
||||
},
|
||||
}] : []),
|
||||
$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
|
||||
icon: 'ti ti-language-hiragana',
|
||||
text: i18n.ts.translate,
|
||||
|
|
@ -484,7 +485,7 @@ export function getRenoteMenu(props: {
|
|||
}]);
|
||||
}
|
||||
|
||||
if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
|
||||
if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) {
|
||||
normalRenoteItems.push(...[{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function isSupportShare(): boolean {
|
||||
return 'share' in navigator;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey 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,
|
||||
}, '*');
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ export function play(operationType: OperationType) {
|
|||
if (sound.type == null || !canPlay) return;
|
||||
|
||||
canPlay = false;
|
||||
playFile(sound).then(() => {
|
||||
playFile(sound).finally(() => {
|
||||
// ごく短時間に音が重複しないように
|
||||
setTimeout(() => {
|
||||
canPlay = true;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const getBuiltinThemes = () => Promise.all(
|
|||
'd-cherry',
|
||||
'd-ice',
|
||||
'd-u0',
|
||||
].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
].map(name => import(`${__dirname}/../themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
|
||||
export const getBuiltinThemesRef = () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export type PageHeaderItem = {
|
||||
text: string;
|
||||
icon: string;
|
||||
highlighted?: boolean;
|
||||
handler: (ev: MouseEvent) => void;
|
||||
};
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey 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 'misskey-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',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -150,10 +150,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',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ import cssnano from 'cssnano';
|
|||
import postcss from 'postcss';
|
||||
import * as terser from 'terser';
|
||||
|
||||
import locales from '../locales/index.js';
|
||||
import { build as buildLocales } from '../locales/index.js';
|
||||
import generateDTS from '../locales/generateDTS.js';
|
||||
import meta from '../package.json' assert { type: "json" };
|
||||
|
||||
let locales = buildLocales();
|
||||
|
||||
async function copyFrontendFonts() {
|
||||
await fs.cp('./packages/frontend/node_modules/three/examples/fonts', './built/_frontend_dist_/fonts', { dereference: true, recursive: true });
|
||||
}
|
||||
|
|
@ -89,10 +91,12 @@ async function build() {
|
|||
await build();
|
||||
|
||||
if (process.argv.includes("--watch")) {
|
||||
const watcher = fs.watch('./packages', { recursive: true });
|
||||
for await (const event of watcher) {
|
||||
if (/^[a-z]+\/src/.test(event.filename)) {
|
||||
await build();
|
||||
}
|
||||
}
|
||||
const watcher = fs.watch('./locales');
|
||||
for await (const event of watcher) {
|
||||
const filename = event.filename?.replaceAll('\\', '/');
|
||||
if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) {
|
||||
locales = buildLocales();
|
||||
await copyFrontendLocales()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue