mirror of
https://github.com/kokonect-link/cherrypick
synced 2024-11-27 14:28:53 +09:00
Merge remote-branch 'misskey/develop'
This commit is contained in:
commit
feef0d6ffd
5
.github/workflows/get-api-diff.yml
vendored
5
.github/workflows/get-api-diff.yml
vendored
@ -22,16 +22,13 @@ jobs:
|
|||||||
api-json-name: [api-base.json, api-head.json]
|
api-json-name: [api-base.json, api-head.json]
|
||||||
include:
|
include:
|
||||||
- api-json-name: api-base.json
|
- api-json-name: api-base.json
|
||||||
repo-name: ${{ github.event.pull_request.base.repo.full_name }}
|
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
- api-json-name: api-head.json
|
- api-json-name: api-head.json
|
||||||
repo-name: ${{ github.event.pull_request.head.repo.full_name }}
|
ref: refs/pull/${{ github.event.number }}/merge
|
||||||
ref: ${{ github.head_ref }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
repository: ${{ matrix.repo-name }}
|
|
||||||
ref: ${{ matrix.ref }}
|
ref: ${{ matrix.ref }}
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
|
@ -21,15 +21,22 @@
|
|||||||
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加
|
||||||
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
||||||
- Enhance: ユーザーのRawデータを表示するページが復活
|
- Enhance: ユーザーのRawデータを表示するページが復活
|
||||||
- Enhance: リアクション選択時に音を鳴らせるように
|
- Enhance: リアクション選択時に音を鳴らせるように
|
||||||
- Enhance: サウンドにドライブのファイルを使用できるように
|
- Enhance: サウンドにドライブのファイルを使用できるように
|
||||||
|
- Enhance: ナビゲーションバーに項目「キャッシュを削除」を追加
|
||||||
|
- Enhance: Shareページで投稿を完了すると、親ウィンドウ(親フレーム)にpostMessageするように
|
||||||
|
- Enhance: チャンネル、クリップ、ページ、Play、ギャラリーにURLのコピーボタンを設置 #11305
|
||||||
|
- Enhance: ノートプレビューに「内容を隠す」が反映されるように
|
||||||
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
||||||
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
|
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
|
||||||
|
- Enhance: 絵文字の詳細ページに記載される情報を追加
|
||||||
- Fix: コードエディタが正しく表示されない問題を修正
|
- Fix: コードエディタが正しく表示されない問題を修正
|
||||||
- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正
|
- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正
|
||||||
- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正
|
- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正
|
||||||
|
- Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305
|
||||||
- Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470
|
- Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
@ -67,8 +67,8 @@ RUN apt-get update \
|
|||||||
&& corepack enable \
|
&& corepack enable \
|
||||||
&& groupadd -g "${GID}" cherrypick \
|
&& groupadd -g "${GID}" cherrypick \
|
||||||
&& useradd -l -u "${UID}" -g "${GID}" -m -d /cherrypick 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 /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 /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 /g+s -ignore_readdir_race -exec chmod g-s {} \; \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists
|
&& rm -rf /var/lib/apt/lists
|
||||||
|
|
||||||
|
3
locales/index.d.ts
vendored
3
locales/index.d.ts
vendored
@ -1130,6 +1130,8 @@ export interface Locale {
|
|||||||
"sensitiveWords": string;
|
"sensitiveWords": string;
|
||||||
"sensitiveWordsDescription": string;
|
"sensitiveWordsDescription": string;
|
||||||
"sensitiveWordsDescription2": string;
|
"sensitiveWordsDescription2": string;
|
||||||
|
"hiddenTags": string;
|
||||||
|
"hiddenTagsDescription": string;
|
||||||
"notesSearchNotAvailable": string;
|
"notesSearchNotAvailable": string;
|
||||||
"license": string;
|
"license": string;
|
||||||
"unfavoriteConfirm": string;
|
"unfavoriteConfirm": string;
|
||||||
@ -2423,6 +2425,7 @@ export interface Locale {
|
|||||||
"chooseList": string;
|
"chooseList": string;
|
||||||
};
|
};
|
||||||
"clicker": string;
|
"clicker": string;
|
||||||
|
"birthdayFollowings": string;
|
||||||
};
|
};
|
||||||
"_cw": {
|
"_cw": {
|
||||||
"hide": string;
|
"hide": string;
|
||||||
|
@ -1127,6 +1127,8 @@ resetPasswordConfirm: "パスワードリセットしますか?"
|
|||||||
sensitiveWords: "センシティブワード"
|
sensitiveWords: "センシティブワード"
|
||||||
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||||
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
||||||
|
hiddenTags: "非表示ハッシュタグ"
|
||||||
|
hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。"
|
||||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||||
license: "ライセンス"
|
license: "ライセンス"
|
||||||
unfavoriteConfirm: "お気に入り解除しますか?"
|
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||||
@ -2324,6 +2326,7 @@ _widgets:
|
|||||||
_userList:
|
_userList:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
clicker: "クリッカー"
|
clicker: "クリッカー"
|
||||||
|
birthdayFollowings: "今日誕生日のユーザー"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
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;
|
public location: string | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
@Column('char', {
|
@Column('char', {
|
||||||
length: 10, nullable: true,
|
length: 10, nullable: true,
|
||||||
comment: 'The birthday (YYYY-MM-DD) of the User.',
|
comment: 'The birthday (YYYY-MM-DD) of the User.',
|
||||||
|
@ -33,13 +33,7 @@ export const meta = {
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
properties: {
|
ref: 'InviteCode',
|
||||||
code: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
example: 'GR6S02ERUA5VR',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -21,6 +21,7 @@ export const meta = {
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
ref: 'InviteCode',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -31,13 +31,7 @@ export const meta = {
|
|||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
properties: {
|
ref: 'InviteCode',
|
||||||
code: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
example: 'GR6S02ERUA5VR',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import type { RegistrationTicketsRepository } from '@/models/_.js';
|
|||||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
@ -23,6 +22,7 @@ export const meta = {
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
ref: 'InviteCode',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -42,6 +42,12 @@ export const meta = {
|
|||||||
code: 'FORBIDDEN',
|
code: 'FORBIDDEN',
|
||||||
id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba',
|
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;
|
} as const;
|
||||||
|
|
||||||
@ -59,6 +65,8 @@ export const paramDef = {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'The local host is represented with `null`.',
|
description: 'The local host is represented with `null`.',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
birthday: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{ required: ['userId'] },
|
{ required: ['userId'] },
|
||||||
@ -117,6 +125,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
.andWhere('following.followerId = :userId', { userId: user.id })
|
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||||
.innerJoinAndSelect('following.followee', 'followee');
|
.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
|
const followings = await query
|
||||||
.limit(ps.limit)
|
.limit(ps.limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
@ -16,7 +16,22 @@ import MkButton from '@/components/MkButton.vue';
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
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<{
|
const emit = defineEmits<{
|
||||||
@ -25,9 +40,9 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
return concat([
|
return concat([
|
||||||
props.note.text ? [i18n.t('_cw.chars', { count: props.note.text.length })] : [],
|
props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [],
|
||||||
props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length })] : [],
|
props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [],
|
||||||
props.note.poll != null ? [i18n.ts.poll] : [],
|
props.poll != null ? [i18n.ts.poll] : [],
|
||||||
] as string[][]).join(' / ');
|
] as string[][]).join(' / ');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkEvent v-if="appearNote.event" :note="appearNote"/>
|
<MkEvent v-if="appearNote.event" :note="appearNote"/>
|
||||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||||
<Mfm v-if="appearNote.cw != ''" :text="appearNote.cw" :author="appearNote.user" :nyaize="noNyaize ? false : 'respect'" @click.stop/>
|
<Mfm v-if="appearNote.cw != ''" :text="appearNote.cw" :author="appearNote.user" :nyaize="noNyaize ? false : 'respect'" @click.stop/>
|
||||||
<MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" @click.stop/>
|
<MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
|
||||||
</p>
|
</p>
|
||||||
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
|
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
|
||||||
<div :class="$style.text">
|
<div :class="$style.text">
|
||||||
|
@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkEvent v-if="appearNote.event" :note="appearNote"/>
|
<MkEvent v-if="appearNote.event" :note="appearNote"/>
|
||||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
<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'"/>
|
<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>
|
</p>
|
||||||
<div v-show="appearNote.cw == null || showContent">
|
<div v-show="appearNote.cw == null || showContent">
|
||||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts._ffVisibility.private }})</span>
|
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts._ffVisibility.private }})</span>
|
||||||
|
@ -11,7 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkUserName :user="user" :nowrap="true"/>
|
<MkUserName :user="user" :nowrap="true"/>
|
||||||
</div>
|
</div>
|
||||||
<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"/>
|
<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"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||||
</div>
|
</div>
|
||||||
@ -21,15 +25,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'cherrypick-js';
|
import * as Misskey from 'cherrypick-js';
|
||||||
import * as mfm from 'cherrypick-mfm-js';
|
import * as mfm from 'cherrypick-mfm-js';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
|
import MkCwButton from '@/components/MkCwButton.vue';
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
const showContent = ref(false);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
text: string;
|
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;
|
user: Misskey.entities.User;
|
||||||
showProfile?: boolean;
|
showProfile?: boolean;
|
||||||
}>();
|
}>();
|
||||||
@ -62,6 +78,14 @@ const urls = props.text ? extractUrlFromMfm(mfm.parse(props.text)) : null;
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cw {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkEvent v-if="note.event" :note="note"/>
|
<MkEvent v-if="note.event" :note="note"/>
|
||||||
<p v-if="note.cw != null" :class="$style.cw">
|
<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"/>
|
<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>
|
</p>
|
||||||
<div v-show="note.cw == null || showContent">
|
<div v-show="note.cw == null || showContent">
|
||||||
<MkSubNoteContent :class="$style.text" :note="note"/>
|
<MkSubNoteContent :class="$style.text" :note="note"/>
|
||||||
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkEvent v-if="note.event" :note="note"/>
|
<MkEvent v-if="note.event" :note="note"/>
|
||||||
<p v-if="note.cw != null" :class="$style.cw">
|
<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'" @click.stop/>
|
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" @click.stop/>
|
||||||
<MkCwButton v-model="showContent" style="width: 100%" :note="note" @click.stop/>
|
<MkCwButton v-model="showContent" style="width: 100%" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/>
|
||||||
</p>
|
</p>
|
||||||
<div v-show="note.cw == null || showContent">
|
<div v-show="note.cw == null || showContent">
|
||||||
<MkSubNoteContent :class="$style.text" :note="note" :showSubNoteFooterButton="defaultStore.state.showSubNoteFooterButton"/>
|
<MkSubNoteContent :class="$style.text" :note="note" :showSubNoteFooterButton="defaultStore.state.showSubNoteFooterButton"/>
|
||||||
|
@ -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">
|
<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"/>
|
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
<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 v-if="showingOptions" style="padding: 8px 16px;">
|
||||||
</div>
|
</div>
|
||||||
<footer :class="$style.footer">
|
<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">
|
<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"/>
|
<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"/>
|
<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 v-if="showingOptions && showForm" style="padding: 8px 16px;">
|
||||||
</div>
|
</div>
|
||||||
<Transition
|
<Transition
|
||||||
|
@ -70,6 +70,7 @@ import { mainRouter } from '@/router.js';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { PageHeaderItem } from '@/types/page-header.js';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
|
|
||||||
let showFollowButton = $ref(false);
|
let showFollowButton = $ref(false);
|
||||||
@ -88,12 +89,7 @@ type Tab = {
|
|||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
tabs?: Tab[];
|
tabs?: Tab[];
|
||||||
tab?: string;
|
tab?: string;
|
||||||
actions?: {
|
actions?: PageHeaderItem[];
|
||||||
text: string;
|
|
||||||
icon: string;
|
|
||||||
highlighted?: boolean;
|
|
||||||
handler: (ev: MouseEvent) => void;
|
|
||||||
}[];
|
|
||||||
thin?: boolean;
|
thin?: boolean;
|
||||||
displayMyAvatar?: boolean;
|
displayMyAvatar?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
@ -104,7 +104,7 @@ export default function(props: MfmProps) {
|
|||||||
|
|
||||||
case 'fn': {
|
case 'fn': {
|
||||||
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
|
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
|
||||||
let style;
|
let style: string | undefined;
|
||||||
switch (token.props.name) {
|
switch (token.props.name) {
|
||||||
case 'tada': {
|
case 'tada': {
|
||||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
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), ']']);
|
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
|
||||||
} else {
|
} else {
|
||||||
return h('span', {
|
return h('span', {
|
||||||
|
@ -74,6 +74,7 @@ import { mainRouter } from '@/router.js';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { PageHeaderItem } from '@/types/page-header.js';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
|
|
||||||
let showFollowButton = $ref(false);
|
let showFollowButton = $ref(false);
|
||||||
@ -84,12 +85,7 @@ const canBack = ref(['index', 'explore', 'my-notifications', 'messaging'].includ
|
|||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
tabs?: Tab[];
|
tabs?: Tab[];
|
||||||
tab?: string;
|
tab?: string;
|
||||||
actions?: {
|
actions?: PageHeaderItem[];
|
||||||
text: string;
|
|
||||||
icon: string;
|
|
||||||
highlighted?: boolean;
|
|
||||||
handler: (ev: MouseEvent) => void;
|
|
||||||
}[];
|
|
||||||
thin?: boolean;
|
thin?: boolean;
|
||||||
displayMyAvatar?: boolean;
|
displayMyAvatar?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, defineAsyncComponent, reactive } from 'vue';
|
import { computed, defineAsyncComponent, reactive } from 'vue';
|
||||||
import { clearCache } from './scripts/clear-cache.js';
|
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
|
import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
|
||||||
@ -14,6 +13,7 @@ import { i18n } from '@/i18n.js';
|
|||||||
import { ui } from '@/config.js';
|
import { ui } from '@/config.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||||
|
import { clearCache } from './scripts/clear-cache.js';
|
||||||
|
|
||||||
export const navbarItemDef = reactive({
|
export const navbarItemDef = reactive({
|
||||||
notifications: {
|
notifications: {
|
||||||
|
@ -39,6 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
||||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
|
<MkTextarea v-model="hiddenTags">
|
||||||
|
<template #label>{{ i18n.ts.hiddenTags }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
|
||||||
|
</MkTextarea>
|
||||||
</div>
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
@ -72,6 +77,7 @@ import FormLink from '@/components/form/link.vue';
|
|||||||
let enableRegistration: boolean = $ref(false);
|
let enableRegistration: boolean = $ref(false);
|
||||||
let emailRequiredForSignup: boolean = $ref(false);
|
let emailRequiredForSignup: boolean = $ref(false);
|
||||||
let sensitiveWords: string = $ref('');
|
let sensitiveWords: string = $ref('');
|
||||||
|
let hiddenTags: string = $ref('');
|
||||||
let preservedUsernames: string = $ref('');
|
let preservedUsernames: string = $ref('');
|
||||||
let tosUrl: string | null = $ref(null);
|
let tosUrl: string | null = $ref(null);
|
||||||
let privacyPolicyUrl: string | null = $ref(null);
|
let privacyPolicyUrl: string | null = $ref(null);
|
||||||
@ -81,6 +87,7 @@ async function init() {
|
|||||||
enableRegistration = !meta.disableRegistration;
|
enableRegistration = !meta.disableRegistration;
|
||||||
emailRequiredForSignup = meta.emailRequiredForSignup;
|
emailRequiredForSignup = meta.emailRequiredForSignup;
|
||||||
sensitiveWords = meta.sensitiveWords.join('\n');
|
sensitiveWords = meta.sensitiveWords.join('\n');
|
||||||
|
hiddenTags = meta.hiddenTags.join('\n');
|
||||||
preservedUsernames = meta.preservedUsernames.join('\n');
|
preservedUsernames = meta.preservedUsernames.join('\n');
|
||||||
tosUrl = meta.tosUrl;
|
tosUrl = meta.tosUrl;
|
||||||
privacyPolicyUrl = meta.privacyPolicyUrl;
|
privacyPolicyUrl = meta.privacyPolicyUrl;
|
||||||
@ -93,6 +100,7 @@ function save() {
|
|||||||
tosUrl,
|
tosUrl,
|
||||||
privacyPolicyUrl,
|
privacyPolicyUrl,
|
||||||
sensitiveWords: sensitiveWords.split('\n'),
|
sensitiveWords: sensitiveWords.split('\n'),
|
||||||
|
hiddenTags: hiddenTags.split('\n'),
|
||||||
preservedUsernames: preservedUsernames.split('\n'),
|
preservedUsernames: preservedUsernames.split('\n'),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
|
@ -86,6 +86,9 @@ import { defaultStore } from '@/store.js';
|
|||||||
import MkNote from '@/components/MkNote.vue';
|
import MkNote from '@/components/MkNote.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.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();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -167,24 +170,40 @@ async function search() {
|
|||||||
|
|
||||||
const headerActions = $computed(() => {
|
const headerActions = $computed(() => {
|
||||||
if (channel && channel.userId) {
|
if (channel && channel.userId) {
|
||||||
const share = {
|
const headerItems: PageHeaderItem[] = [];
|
||||||
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 canEdit = ($i && $i.id === channel.userId) || iAmModerator;
|
headerItems.push({
|
||||||
return canEdit ? [share, {
|
icon: 'ti ti-link',
|
||||||
icon: 'ti ti-settings',
|
text: i18n.ts.copyUrl,
|
||||||
text: i18n.ts.edit,
|
handler: async (): Promise<void> => {
|
||||||
handler: edit,
|
copyToClipboard(`${url}/channels/${channel.id}`);
|
||||||
}] : [share];
|
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 {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||||||
import { url } from '@/config.js';
|
import { url } from '@/config.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { clipsCache } from '@/cache';
|
import { clipsCache } from '@/cache';
|
||||||
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
clipId: string,
|
clipId: string,
|
||||||
@ -118,6 +120,13 @@ const headerActions = $computed(() => clip && isOwned ? [{
|
|||||||
clipsCache.delete();
|
clipsCache.delete();
|
||||||
},
|
},
|
||||||
}, ...(clip.isPublic ? [{
|
}, ...(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',
|
icon: 'ti ti-share',
|
||||||
text: i18n.ts.share,
|
text: i18n.ts.share,
|
||||||
handler: async (): Promise<void> => {
|
handler: async (): Promise<void> => {
|
||||||
|
@ -46,7 +46,7 @@ function menu(ev) {
|
|||||||
os.apiGet('emoji', { name: props.emoji.name }).then(res => {
|
os.apiGet('emoji', { name: props.emoji.name }).then(res => {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'info',
|
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-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-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.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>
|
</div>
|
||||||
<div v-else :class="$style.ready">
|
<div v-else :class="$style.ready">
|
||||||
@ -70,6 +71,8 @@ import MkFolder from '@/components/MkFolder.vue';
|
|||||||
import MkCode from '@/components/MkCode.vue';
|
import MkCode from '@/components/MkCode.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id: string;
|
id: string;
|
||||||
@ -89,6 +92,11 @@ function fetchFlash() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyLink() {
|
||||||
|
copyToClipboard(`${url}/play/${flash.id}`);
|
||||||
|
os.success();
|
||||||
|
}
|
||||||
|
|
||||||
function share() {
|
function share() {
|
||||||
navigator.share({
|
navigator.share({
|
||||||
title: flash.title,
|
title: flash.title,
|
||||||
|
@ -29,7 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div class="other">
|
<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-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.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>
|
</div>
|
||||||
<div class="user">
|
<div class="user">
|
||||||
@ -74,6 +75,8 @@ import { i18n } from '@/i18n.js';
|
|||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -102,6 +105,11 @@ function fetchPost() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyLink() {
|
||||||
|
copyToClipboard(`${url}/gallery/${post.id}`);
|
||||||
|
os.success();
|
||||||
|
}
|
||||||
|
|
||||||
function share() {
|
function share() {
|
||||||
navigator.share({
|
navigator.share({
|
||||||
title: post.title,
|
title: post.title,
|
||||||
|
@ -34,7 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
<div class="other">
|
<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.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>
|
</div>
|
||||||
<div class="user">
|
<div class="user">
|
||||||
@ -90,6 +91,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||||||
import { pageViewInterruptors, defaultStore } from '@/store.js';
|
import { pageViewInterruptors, defaultStore } from '@/store.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
pageName: string;
|
pageName: string;
|
||||||
@ -136,6 +139,11 @@ function share() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyLink() {
|
||||||
|
copyToClipboard(`${url}/@${page.user.username}/pages/${page.name}`);
|
||||||
|
os.success();
|
||||||
|
}
|
||||||
|
|
||||||
function shareWithNote() {
|
function shareWithNote() {
|
||||||
os.post({
|
os.post({
|
||||||
initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`,
|
initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`,
|
||||||
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:renote="renote"
|
:renote="renote"
|
||||||
:initialVisibleUsers="visibleUsers"
|
:initialVisibleUsers="visibleUsers"
|
||||||
class="_panel"
|
class="_panel"
|
||||||
@posted="state = 'posted'"
|
@posted="onPosted"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="state === 'posted'" class="_buttonsCenter">
|
<div v-else-if="state === 'posted'" class="_buttonsCenter">
|
||||||
<MkButton primary @click="close">{{ i18n.ts.close }}</MkButton>
|
<MkButton primary @click="close">{{ i18n.ts.close }}</MkButton>
|
||||||
@ -32,20 +32,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// SPECIFICATION: https://misskey-hub.net/docs/features/share-form.html
|
// SPECIFICATION: https://misskey-hub.net/docs/features/share-form.html
|
||||||
|
|
||||||
import { } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'cherrypick-js';
|
import * as Misskey from 'cherrypick-js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import { postMessageToParentWindow } from '@/scripts/post-message.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const localOnlyQuery = urlParams.get('localOnly');
|
const localOnlyQuery = urlParams.get('localOnly');
|
||||||
const visibilityQuery = urlParams.get('visibility') as typeof Misskey.noteVisibilities[number];
|
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'));
|
let title = $ref(urlParams.get('title'));
|
||||||
const text = urlParams.get('text');
|
const text = urlParams.get('text');
|
||||||
const url = urlParams.get('url');
|
const url = urlParams.get('url');
|
||||||
@ -144,7 +144,7 @@ async function init() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
state = 'writing';
|
state.value = 'writing';
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
@ -162,6 +162,11 @@ function goToMisskey(): void {
|
|||||||
location.href = '/';
|
location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPosted(): void {
|
||||||
|
state.value = 'posted';
|
||||||
|
postMessageToParentWindow('misskey:shareForm:shareCompleted');
|
||||||
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
@ -18,6 +18,7 @@ import { getUserMenu } from '@/scripts/get-user-menu.js';
|
|||||||
import { clipsCache } from '@/cache.js';
|
import { clipsCache } from '@/cache.js';
|
||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
|
|
||||||
export async function getNoteClipMenu(props: {
|
export async function getNoteClipMenu(props: {
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
@ -339,11 +340,12 @@ export function getNoteMenu(props: {
|
|||||||
danger: true,
|
danger: true,
|
||||||
action: unclip,
|
action: unclip,
|
||||||
}, null] : []
|
}, null] : []
|
||||||
), {
|
), ...(isSupportShare() ? [{
|
||||||
icon: 'ti ti-share',
|
icon: 'ti ti-share',
|
||||||
text: i18n.ts.share,
|
text: i18n.ts.share,
|
||||||
action: share,
|
action: share,
|
||||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
|
}] : []),
|
||||||
|
getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
|
||||||
, {
|
, {
|
||||||
icon: 'ti ti-copy',
|
icon: 'ti ti-copy',
|
||||||
text: i18n.ts.copyContent,
|
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,
|
||||||
|
}, '*');
|
||||||
|
}
|
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('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue')));
|
||||||
app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue')));
|
app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue')));
|
||||||
app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue')));
|
app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue')));
|
||||||
|
app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue')));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const widgets = [
|
export const widgets = [
|
||||||
@ -63,4 +64,5 @@ export const widgets = [
|
|||||||
'aichan',
|
'aichan',
|
||||||
'userList',
|
'userList',
|
||||||
'clicker',
|
'clicker',
|
||||||
|
'birthdayFollowings',
|
||||||
];
|
];
|
||||||
|
@ -91,16 +91,10 @@ async function build() {
|
|||||||
await build();
|
await build();
|
||||||
|
|
||||||
if (process.argv.includes("--watch")) {
|
if (process.argv.includes("--watch")) {
|
||||||
const watcher = fs.watch('./', { recursive: true });
|
const watcher = fs.watch('./locales');
|
||||||
|
|
||||||
for await (const event of watcher) {
|
for await (const event of watcher) {
|
||||||
const filename = event.filename?.replaceAll('\\', '/');
|
const filename = event.filename?.replaceAll('\\', '/');
|
||||||
|
if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) {
|
||||||
if (/^packages\/[a-z]+\/src/.test(filename)) {
|
|
||||||
await build();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^locales\/[a-z]+-[A-Z]+\.yml/.test(filename)) {
|
|
||||||
locales = buildLocales();
|
locales = buildLocales();
|
||||||
await copyFrontendLocales()
|
await copyFrontendLocales()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user