Merge 97aa84b311
into 7d7a12d7d6
This commit is contained in:
commit
59b0f0191b
|
@ -9,6 +9,8 @@
|
||||||
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
|
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
|
||||||
- Enhance: アイコンデコレーション管理画面にプレビューを追加
|
- Enhance: アイコンデコレーション管理画面にプレビューを追加
|
||||||
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
|
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
|
||||||
|
- Enhance: どこで投稿フォームを開いてもお気に入りに登録したチャンネルにノートできるように
|
||||||
|
- Enhance: チャンネルのページを開いている間はデフォルトの公開範囲がそのチャンネルになるように
|
||||||
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
||||||
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
|
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
|
||||||
|
|
||||||
|
|
|
@ -8543,6 +8543,18 @@ export interface Locale extends ILocale {
|
||||||
* 指定したユーザーのみに公開
|
* 指定したユーザーのみに公開
|
||||||
*/
|
*/
|
||||||
"specifiedDescription": string;
|
"specifiedDescription": string;
|
||||||
|
/**
|
||||||
|
* チャンネル
|
||||||
|
*/
|
||||||
|
"channel": string;
|
||||||
|
/**
|
||||||
|
* 選択したチャンネルに公開
|
||||||
|
*/
|
||||||
|
"channelDescription": string;
|
||||||
|
/**
|
||||||
|
* 選択中:{name}
|
||||||
|
*/
|
||||||
|
"channelSelected": ParameterizedString<"name">;
|
||||||
/**
|
/**
|
||||||
* 連合なし
|
* 連合なし
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2248,6 +2248,9 @@ _visibility:
|
||||||
followersDescription: "自分のフォロワーのみに公開"
|
followersDescription: "自分のフォロワーのみに公開"
|
||||||
specified: "ダイレクト"
|
specified: "ダイレクト"
|
||||||
specifiedDescription: "指定したユーザーのみに公開"
|
specifiedDescription: "指定したユーザーのみに公開"
|
||||||
|
channel: "チャンネル"
|
||||||
|
channelDescription: "選択したチャンネルに公開"
|
||||||
|
channelSelected: "選択中:{name}"
|
||||||
disableFederation: "連合なし"
|
disableFederation: "連合なし"
|
||||||
disableFederationDescription: "他サーバーへの配信を行いません"
|
disableFederationDescription: "他サーバーへの配信を行いません"
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
import { mainRouter } from '@/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
|
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
|
||||||
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
|
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
|
||||||
|
import { postButtonHandler } from '@/scripts/post-button-handler.js';
|
||||||
|
|
||||||
export async function mainBoot() {
|
export async function mainBoot() {
|
||||||
const { isClientUpdated } = await common(() => createApp(
|
const { isClientUpdated } = await common(() => createApp(
|
||||||
|
@ -346,7 +347,7 @@ export async function mainBoot() {
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'p|n': () => {
|
'p|n': () => {
|
||||||
if ($i == null) return;
|
if ($i == null) return;
|
||||||
post();
|
postButtonHandler(mainRouter.currentRef.value);
|
||||||
},
|
},
|
||||||
'd': () => {
|
'd': () => {
|
||||||
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
||||||
|
|
|
@ -19,21 +19,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.headerRight">
|
<div :class="$style.headerRight">
|
||||||
<template v-if="!(channel != null && fixed)">
|
<button ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility">
|
||||||
<button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility">
|
<template v-if="postChannel">
|
||||||
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
|
|
||||||
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
|
|
||||||
<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
|
|
||||||
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
|
|
||||||
<span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span>
|
|
||||||
</button>
|
|
||||||
<button v-else class="_button" :class="[$style.headerRightItem, $style.visibility]" disabled>
|
|
||||||
<span><i class="ti ti-device-tv"></i></span>
|
<span><i class="ti ti-device-tv"></i></span>
|
||||||
<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
|
<span v-if="postChannel" :class="$style.headerRightButtonText">{{ postChannelName }}</span>
|
||||||
</button>
|
</template>
|
||||||
</template>
|
<template v-else>
|
||||||
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
|
<span v-if="actualVisibility === 'public'"><i class="ti ti-world"></i></span>
|
||||||
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
|
<span v-if="actualVisibility === 'home'"><i class="ti ti-home"></i></span>
|
||||||
|
<span v-if="actualVisibility === 'followers'"><i class="ti ti-lock"></i></span>
|
||||||
|
<span v-if="actualVisibility === 'specified'"><i class="ti ti-mail"></i></span>
|
||||||
|
<span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[actualVisibility] }}</span>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: actualLocalOnly }]" :disabled="postChannel != null || actualVisibility === 'specified'" @click="toggleLocalOnly">
|
||||||
|
<span v-if="!actualLocalOnly"><i class="ti ti-rocket"></i></span>
|
||||||
<span v-else><i class="ti ti-rocket-off"></i></span>
|
<span v-else><i class="ti ti-rocket-off"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
|
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
|
||||||
|
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
|
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
|
||||||
<MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/>
|
<MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/>
|
||||||
<div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
|
<div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
|
||||||
<div v-if="visibility === 'specified'" :class="$style.toSpecified">
|
<div v-if="actualVisibility === 'specified'" :class="$style.toSpecified">
|
||||||
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
|
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
|
||||||
<div :class="$style.visibleUsers">
|
<div :class="$style.visibleUsers">
|
||||||
<span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser">
|
<span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser">
|
||||||
|
@ -66,8 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
|
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
|
||||||
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
|
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
|
||||||
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
|
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]" :style="postChannel && postChannel.color ? `--channel-color: ${postChannel.color}` : undefined">
|
||||||
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
|
|
||||||
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
||||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -137,7 +136,7 @@ const modal = inject('modal');
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
reply?: Misskey.entities.Note;
|
reply?: Misskey.entities.Note;
|
||||||
renote?: Misskey.entities.Note;
|
renote?: Misskey.entities.Note;
|
||||||
channel?: Misskey.entities.Channel; // TODO
|
channel?: Misskey.entities.Channel;
|
||||||
mention?: Misskey.entities.User;
|
mention?: Misskey.entities.User;
|
||||||
specified?: Misskey.entities.UserDetailed;
|
specified?: Misskey.entities.UserDetailed;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
|
@ -202,8 +201,22 @@ const imeText = ref('');
|
||||||
const showingOptions = ref(false);
|
const showingOptions = ref(false);
|
||||||
const textAreaReadOnly = ref(false);
|
const textAreaReadOnly = ref(false);
|
||||||
|
|
||||||
|
const postChannel = ref<Misskey.entities.Channel | null>(props.channel ?? null);
|
||||||
|
const postChannelName = computed<string>(() => postChannel.value?.name ?? '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link localOnly}が持つ値にチャンネル選択有無を加味した値を計算する(チャンネル選択時は強制的にfalse)
|
||||||
|
* チャンネル選択有無を考慮する必要がある場面では{@link localOnly}ではなくこの値を使用する。
|
||||||
|
*/
|
||||||
|
const actualLocalOnly = computed<boolean>(() => postChannel.value ? true : localOnly.value);
|
||||||
|
/**
|
||||||
|
* {@link visibility}が持つ値にチャンネル選択有無を加味した値を計算する(チャンネル選択時は強制的にpublic)。
|
||||||
|
* チャンネル選択有無を考慮する必要がある場面では{@link actualVisibility}ではなくこの値を使用する。
|
||||||
|
*/
|
||||||
|
const actualVisibility = computed<typeof Misskey.noteVisibilities[number]>(() => postChannel.value ? 'public' : visibility.value);
|
||||||
|
|
||||||
const draftKey = computed((): string => {
|
const draftKey = computed((): string => {
|
||||||
let key = props.channel ? `channel:${props.channel.id}` : '';
|
let key = postChannel.value ? `channel:${postChannel.value.id}` : '';
|
||||||
|
|
||||||
if (props.renote) {
|
if (props.renote) {
|
||||||
key += `renote:${props.renote.id}`;
|
key += `renote:${props.renote.id}`;
|
||||||
|
@ -221,7 +234,7 @@ const placeholder = computed((): string => {
|
||||||
return i18n.ts._postForm.quotePlaceholder;
|
return i18n.ts._postForm.quotePlaceholder;
|
||||||
} else if (props.reply) {
|
} else if (props.reply) {
|
||||||
return i18n.ts._postForm.replyPlaceholder;
|
return i18n.ts._postForm.replyPlaceholder;
|
||||||
} else if (props.channel) {
|
} else if (postChannel.value) {
|
||||||
return i18n.ts._postForm.channelPlaceholder;
|
return i18n.ts._postForm.channelPlaceholder;
|
||||||
} else {
|
} else {
|
||||||
const xs = [
|
const xs = [
|
||||||
|
@ -272,7 +285,7 @@ watch(text, () => {
|
||||||
checkMissingMention();
|
checkMissingMention();
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
watch(visibility, () => {
|
watch(actualVisibility, () => {
|
||||||
checkMissingMention();
|
checkMissingMention();
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
@ -316,11 +329,6 @@ if ($i.isSilenced && visibility.value === 'public') {
|
||||||
visibility.value = 'home';
|
visibility.value = 'home';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.channel) {
|
|
||||||
visibility.value = 'public';
|
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
||||||
if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
|
if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
|
||||||
if (props.reply.visibility === 'home' && visibility.value === 'followers') {
|
if (props.reply.visibility === 'home' && visibility.value === 'followers') {
|
||||||
|
@ -372,7 +380,7 @@ function watchForDraft() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkMissingMention() {
|
function checkMissingMention() {
|
||||||
if (visibility.value === 'specified') {
|
if (actualVisibility.value === 'specified') {
|
||||||
const ast = mfm.parse(text.value);
|
const ast = mfm.parse(text.value);
|
||||||
|
|
||||||
for (const x of extractMentions(ast)) {
|
for (const x of extractMentions(ast)) {
|
||||||
|
@ -459,36 +467,31 @@ function upload(file: File, name?: string): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVisibility() {
|
function setVisibility() {
|
||||||
if (props.channel) {
|
|
||||||
visibility.value = 'public';
|
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
|
||||||
currentVisibility: visibility.value,
|
currentVisibility: actualVisibility.value,
|
||||||
isSilenced: $i.isSilenced,
|
isSilenced: $i.isSilenced,
|
||||||
localOnly: localOnly.value,
|
// チャンネル→ダイレクトの選択変更ができなくなるので、チャンネル選択中の場合は意図的にfalseを渡すようにする
|
||||||
|
localOnly: postChannel.value ? false : actualLocalOnly.value,
|
||||||
src: visibilityButton.value,
|
src: visibilityButton.value,
|
||||||
|
currentChannel: postChannel.value,
|
||||||
...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}),
|
...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}),
|
||||||
}, {
|
}, {
|
||||||
changeVisibility: v => {
|
changeVisibility: v => {
|
||||||
visibility.value = v;
|
visibility.value = v;
|
||||||
|
postChannel.value = null;
|
||||||
if (defaultStore.state.rememberNoteVisibility) {
|
if (defaultStore.state.rememberNoteVisibility) {
|
||||||
defaultStore.set('visibility', visibility.value);
|
defaultStore.set('visibility', visibility.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
changeChannel: channel => {
|
||||||
|
// computedで読み替えをするので、localOnlyとvisibilityの変更はしない
|
||||||
|
postChannel.value = channel;
|
||||||
|
},
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleLocalOnly() {
|
async function toggleLocalOnly() {
|
||||||
if (props.channel) {
|
|
||||||
visibility.value = 'public';
|
|
||||||
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo');
|
const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo');
|
||||||
|
|
||||||
if (!localOnly.value && neverShowInfo !== 'true') {
|
if (!localOnly.value && neverShowInfo !== 'true') {
|
||||||
|
@ -716,6 +719,15 @@ function saveDraft() {
|
||||||
function deleteDraft() {
|
function deleteDraft() {
|
||||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
||||||
|
|
||||||
|
if (postChannel.value) {
|
||||||
|
// draftKey.valueからchannel:${postChannel.value.id}部分を削除したのがpartialDraftKey
|
||||||
|
// 通常の投稿からチャンネルに切り替えて投稿した際に、通常の投稿の下書きが残ってしまい不自然な挙動になるのを防ぐ
|
||||||
|
const partialDraftKey = draftKey.value.replace(`channel:${postChannel.value.id}`, '');
|
||||||
|
if (draftData[partialDraftKey]) {
|
||||||
|
delete draftData[partialDraftKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delete draftData[draftKey.value];
|
delete draftData[draftKey.value];
|
||||||
|
|
||||||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||||
|
@ -752,7 +764,8 @@ async function post(ev?: MouseEvent) {
|
||||||
text.value.includes('$[scale') ||
|
text.value.includes('$[scale') ||
|
||||||
text.value.includes('$[position');
|
text.value.includes('$[position');
|
||||||
|
|
||||||
if (annoying && visibility.value === 'public') {
|
// チャンネル投稿時はサーバサイド側でpublicに変更されているため警告を出す意味がない
|
||||||
|
if (annoying && actualVisibility.value === 'public' && !postChannel.value) {
|
||||||
const { canceled, result } = await os.actions({
|
const { canceled, result } = await os.actions({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.ts.thisPostMayBeAnnoying,
|
text: i18n.ts.thisPostMayBeAnnoying,
|
||||||
|
@ -781,12 +794,12 @@ async function post(ev?: MouseEvent) {
|
||||||
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
|
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
|
||||||
replyId: props.reply ? props.reply.id : undefined,
|
replyId: props.reply ? props.reply.id : undefined,
|
||||||
renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
|
renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
|
||||||
channelId: props.channel ? props.channel.id : undefined,
|
channelId: postChannel.value ? postChannel.value.id : undefined,
|
||||||
poll: poll.value,
|
poll: poll.value,
|
||||||
cw: useCw.value ? cw.value ?? '' : null,
|
cw: useCw.value ? cw.value ?? '' : null,
|
||||||
localOnly: localOnly.value,
|
localOnly: actualLocalOnly.value,
|
||||||
visibility: visibility.value,
|
visibility: actualVisibility.value,
|
||||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
visibleUserIds: actualVisibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1139,16 +1152,6 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorBar {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 12px;
|
|
||||||
width: 5px;
|
|
||||||
height: 100% ;
|
|
||||||
border-radius: 999px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submitInner {
|
.submitInner {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
|
@ -1277,6 +1280,18 @@ defineExpose({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 9.5px; // 9.5px + 5px + 9.5px = 24px
|
||||||
|
width: 5px;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--channel-color, transparent);
|
||||||
|
border-radius: 2.5px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.withCw {
|
&.withCw {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
@ -1382,6 +1397,11 @@ defineExpose({
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textOuter::before {
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,32 +9,50 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="[$style.label, $style.item]">
|
<div :class="[$style.label, $style.item]">
|
||||||
{{ i18n.ts.visibility }}
|
{{ i18n.ts.visibility }}
|
||||||
</div>
|
</div>
|
||||||
<button key="public" :disabled="isSilenced || isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
|
<button key="public" :disabled="isSilenced || isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'public' && !currentChannel }]" data-index="1" @click="choose('public')">
|
||||||
<div :class="$style.icon"><i class="ti ti-world"></i></div>
|
<div :class="$style.icon"><i class="ti ti-world"></i></div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
|
<div :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</div>
|
||||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
|
<div :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="home" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
|
<button key="home" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'home' && !currentChannel }]" data-index="2" @click="choose('home')">
|
||||||
<div :class="$style.icon"><i class="ti ti-home"></i></div>
|
<div :class="$style.icon"><i class="ti ti-home"></i></div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
|
<div :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</div>
|
||||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
|
<div :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="followers" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
|
<button key="followers" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' && !currentChannel }]" data-index="3" @click="choose('followers')">
|
||||||
<div :class="$style.icon"><i class="ti ti-lock"></i></div>
|
<div :class="$style.icon"><i class="ti ti-lock"></i></div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
|
<div :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</div>
|
||||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span>
|
<div :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')">
|
<button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' && !currentChannel }]" data-index="4" @click="choose('specified')">
|
||||||
<div :class="$style.icon"><i class="ti ti-mail"></i></div>
|
<div :class="$style.icon"><i class="ti ti-mail"></i></div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span>
|
<div :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</div>
|
||||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
|
<div :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
ref="channelsButton"
|
||||||
|
class="_button"
|
||||||
|
:class="[$style.item, $style.channelButton, { [$style.active]: currentChannel }]"
|
||||||
|
:style="currentChannel && currentChannel.color ? `--channel-color: ${currentChannel.color}` : undefined"
|
||||||
|
data-index="5"
|
||||||
|
@click="chooseChannel"
|
||||||
|
>
|
||||||
|
<div :class="$style.icon">
|
||||||
|
<i class="ti ti-device-tv"></i>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.body">
|
||||||
|
<div :class="$style.itemTitle">{{ i18n.ts._visibility.channel }}</div>
|
||||||
|
<div :class="$style.itemDescription">
|
||||||
|
{{ currentChannelName ? i18n.tsx._visibility.channelSelected({ name: currentChannelName }) : i18n.ts._visibility.channelDescription }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,12 +60,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, shallowRef, ref } from 'vue';
|
import { nextTick, shallowRef, ref, computed } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { favoritedChannelsCache } from '@/cache.js';
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
const channelsButton = shallowRef<InstanceType<typeof HTMLButtonElement>>();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
currentVisibility: typeof Misskey.noteVisibilities[number];
|
currentVisibility: typeof Misskey.noteVisibilities[number];
|
||||||
|
@ -55,16 +77,66 @@ const props = withDefaults(defineProps<{
|
||||||
localOnly: boolean;
|
localOnly: boolean;
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
isReplyVisibilitySpecified?: boolean;
|
isReplyVisibilitySpecified?: boolean;
|
||||||
|
currentChannel?: Misskey.entities.Channel;
|
||||||
}>(), {
|
}>(), {
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'changeVisibility', v: typeof Misskey.noteVisibilities[number]): void;
|
(ev: 'changeVisibility', v: typeof Misskey.noteVisibilities[number]): void;
|
||||||
|
(ev: 'changeChannel', v: Misskey.entities.Channel) : void
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const v = ref(props.currentVisibility);
|
const v = ref(props.currentVisibility);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visibility とチャンネルはそれぞれ独立だけど、今のところはチャンネル投稿は連合なしだし公開範囲も変更できないようである
|
||||||
|
|
||||||
|
packages/frontend/src/components/MkPostForm.vue :475
|
||||||
|
if (props.channel) {
|
||||||
|
visibility.value = 'public';
|
||||||
|
localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
const channels = ref<Misskey.entities.Channel[]>([]);
|
||||||
|
const currentChannel = ref<Misskey.entities.Channel | undefined>(props.currentChannel);
|
||||||
|
const currentChannelName = computed<string | null>(() => currentChannel.value?.name ?? null);
|
||||||
|
|
||||||
|
async function fetchChannels() {
|
||||||
|
const res = await favoritedChannelsCache.fetch();
|
||||||
|
channels.value.splice(0, 0, ...res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseChannel() {
|
||||||
|
let selectedChannel: Misskey.entities.Channel | null = null;
|
||||||
|
await os.popupMenu([
|
||||||
|
{
|
||||||
|
type: 'label',
|
||||||
|
text: i18n.ts.selectChannel,
|
||||||
|
},
|
||||||
|
...channels.value.map<MenuItem>(it => ({
|
||||||
|
type: 'button',
|
||||||
|
text: it.name,
|
||||||
|
active: it.id === currentChannel.value?.id,
|
||||||
|
action: (_) => {
|
||||||
|
selectedChannel = it;
|
||||||
|
currentChannel.value = it;
|
||||||
|
},
|
||||||
|
}))],
|
||||||
|
channelsButton.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (selectedChannel) {
|
||||||
|
emit('changeChannel', selectedChannel);
|
||||||
|
nextTick(() => {
|
||||||
|
if (modal.value) modal.value.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
|
function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
|
||||||
v.value = visibility;
|
v.value = visibility;
|
||||||
emit('changeVisibility', visibility);
|
emit('changeVisibility', visibility);
|
||||||
|
@ -72,6 +144,8 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
|
||||||
if (modal.value) modal.value.close();
|
if (modal.value) modal.value.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchChannels();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -142,6 +216,11 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemTitle,
|
||||||
|
.itemDescription {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -155,4 +234,28 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
|
||||||
.itemDescription {
|
.itemDescription {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channelButton {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.itemDescription {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 0;
|
||||||
|
width: 4px;
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
background: var(--channel-color, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.root.asDrawer .channelButton .itemDescription {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -32,6 +32,7 @@ export type Keys =
|
||||||
'message_drafts' |
|
'message_drafts' |
|
||||||
'scratchpad' |
|
'scratchpad' |
|
||||||
'debug' |
|
'debug' |
|
||||||
|
`channel:${string}` |
|
||||||
`miux:${string}` |
|
`miux:${string}` |
|
||||||
`ui:folder:${string}` |
|
`ui:folder:${string}` |
|
||||||
`themes:${string}` |
|
`themes:${string}` |
|
||||||
|
|
|
@ -122,6 +122,7 @@ watch(() => props.channelId, async () => {
|
||||||
channel.value = await misskeyApi('channels/show', {
|
channel.value = await misskeyApi('channels/show', {
|
||||||
channelId: props.channelId,
|
channelId: props.channelId,
|
||||||
});
|
});
|
||||||
|
miLocalStorage.setItem(`channel:${props.channelId}`, JSON.stringify(channel.value));
|
||||||
favorited.value = channel.value.isFavorited ?? false;
|
favorited.value = channel.value.isFavorited ?? false;
|
||||||
if (favorited.value || channel.value.isFollowing) {
|
if (favorited.value || channel.value.isFollowing) {
|
||||||
tab.value = 'timeline';
|
tab.value = 'timeline';
|
||||||
|
|
|
@ -332,6 +332,7 @@ const routes: RouteDef[] = [{
|
||||||
component: page(() => import('@/pages/channel-editor.vue')),
|
component: page(() => import('@/pages/channel-editor.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
}, {
|
}, {
|
||||||
|
name: 'channel',
|
||||||
path: '/channels/:channelId',
|
path: '/channels/:channelId',
|
||||||
component: page(() => import('@/pages/channel.vue')),
|
component: page(() => import('@/pages/channel.vue')),
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Resolved } from '@/nirax.js';
|
||||||
|
import { post } from '@/os.js';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
|
||||||
|
/** 「ノート」というボタンを押させるときはこっちを呼ぶ */
|
||||||
|
export async function postButtonHandler(currentRef: Resolved) {
|
||||||
|
if (currentRef.route.name === 'channel') {
|
||||||
|
const channelId = currentRef.props.get('channelId');
|
||||||
|
if (typeof channelId === 'string') {
|
||||||
|
// NOTE: チャンネルを開いているならば、チャンネルの情報がキャッシュされていることを期待できるはずである
|
||||||
|
const channelJSON = miLocalStorage.getItem(`channel:${channelId}`);
|
||||||
|
if (channelJSON) {
|
||||||
|
const channel = JSON.parse(channelJSON);
|
||||||
|
if (channel) {
|
||||||
|
await post({ channel });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
miLocalStorage.removeItem(`channel:${channelId}`);
|
||||||
|
}
|
||||||
|
const channel = await misskeyApi('channels/show', { channelId });
|
||||||
|
await post({ channel });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await post();
|
||||||
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.bottom">
|
<div :class="$style.bottom">
|
||||||
<button class="_button" :class="$style.post" data-cy-open-post-form @click="os.post">
|
<button class="_button" :class="$style.post" data-cy-open-post-form @click="post">
|
||||||
<i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span>
|
<i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="_button" :class="$style.account" @click="openAccountMenu">
|
<button class="_button" :class="$style.account" @click="openAccountMenu">
|
||||||
|
@ -57,7 +57,10 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
|
import { useRouter } from '@/router/supplier.js';
|
||||||
|
import { postButtonHandler } from '@/scripts/post-button-handler.js';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const menu = toRef(defaultStore.state, 'menu');
|
const menu = toRef(defaultStore.state, 'menu');
|
||||||
const otherMenuItemIndicated = computed(() => {
|
const otherMenuItemIndicated = computed(() => {
|
||||||
for (const def in navbarItemDef) {
|
for (const def in navbarItemDef) {
|
||||||
|
@ -78,6 +81,10 @@ function more() {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function post() {
|
||||||
|
postButtonHandler(router.currentRef.value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.bottom">
|
<div :class="$style.bottom">
|
||||||
<button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post">
|
<button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="post">
|
||||||
<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
|
<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
|
<button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
|
||||||
|
@ -68,7 +68,10 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
|
import { useRouter } from '@/router/supplier.js';
|
||||||
|
import { postButtonHandler } from '@/scripts/post-button-handler.js';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const iconOnly = ref(false);
|
const iconOnly = ref(false);
|
||||||
|
|
||||||
const menu = computed(() => defaultStore.state.menu);
|
const menu = computed(() => defaultStore.state.menu);
|
||||||
|
@ -105,6 +108,10 @@ function more(ev: MouseEvent) {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function post() {
|
||||||
|
postButtonHandler(router.currentRef.value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
Loading…
Reference in New Issue