Merge branch 'feature/post-channel-everywhere' into develop_test

This commit is contained in:
mesi 2024-01-20 13:15:45 +00:00
commit af3a503d2a
4 changed files with 128 additions and 53 deletions

6
locales/index.d.ts vendored
View file

@ -8173,9 +8173,9 @@ export interface Locale extends ILocale {
*
*/
"specifiedDescription": string;
/**
*
*/
"channel": string;
"channelDescription": string;
"channelSelected": string;
"disableFederation": string;
/**
*

View file

@ -2153,6 +2153,9 @@ _visibility:
followersDescription: "自分のフォロワーのみに公開"
specified: "ダイレクト"
specifiedDescription: "指定したユーザーのみに公開"
channel: "チャンネル"
channelDescription: "選択したチャンネルに公開"
channelSelected: "選択中:{name}"
disableFederation: "連合なし"
disableFederationDescription: "他サーバーへの配信を行いません"

View file

@ -19,21 +19,21 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</div>
<div :class="$style.headerRight">
<template v-if="!(channel != null && fixed)">
<button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility">
<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>
<button ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility">
<template v-if="postChannel">
<span><i class="ti ti-device-tv"></i></span>
<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
</button>
</template>
<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="!localOnly"><i class="ti ti-rocket"></i></span>
<span v-if="postChannel" :class="$style.headerRightButtonText">{{ postChannelName }}</span>
</template>
<template v-else>
<span v-if="actualVisibility === 'public'"><i class="ti ti-world"></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>
</button>
<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="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="visibility === 'specified'" :class="$style.toSpecified">
<div v-if="actualVisibility === 'specified'" :class="$style.toSpecified">
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
<div :class="$style.visibleUsers">
<span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser">
@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
<div v-if="postChannel" :class="$style.colorBar" :style="{ background: postChannel.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"/>
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
@ -137,7 +137,7 @@ const modal = inject('modal');
const props = withDefaults(defineProps<{
reply?: Misskey.entities.Note;
renote?: Misskey.entities.Note;
channel?: Misskey.entities.Channel; // TODO
channel?: Misskey.entities.Channel;
mention?: Misskey.entities.User;
specified?: Misskey.entities.User;
initialText?: string;
@ -206,8 +206,22 @@ const imeText = ref('');
const showingOptions = 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 => {
let key = props.channel ? `channel:${props.channel.id}` : '';
let key = postChannel.value ? `channel:${postChannel.value.id}` : '';
if (props.renote) {
key += `renote:${props.renote.id}`;
@ -225,7 +239,7 @@ const placeholder = computed((): string => {
return i18n.ts._postForm.quotePlaceholder;
} else if (props.reply) {
return i18n.ts._postForm.replyPlaceholder;
} else if (props.channel) {
} else if (postChannel.value) {
return i18n.ts._postForm.channelPlaceholder;
} else {
const xs = [
@ -270,7 +284,7 @@ watch(text, () => {
checkMissingMention();
}, { immediate: true });
watch(visibility, () => {
watch(actualVisibility, () => {
checkMissingMention();
}, { immediate: true });
@ -314,11 +328,6 @@ if ($i.isSilenced && visibility.value === 'public') {
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.visibility === 'home' && visibility.value === 'followers') {
@ -368,7 +377,7 @@ function watchForDraft() {
}
function checkMissingMention() {
if (visibility.value === 'specified') {
if (actualVisibility.value === 'specified') {
const ast = mfm.parse(text.value);
for (const x of extractMentions(ast)) {
@ -455,34 +464,29 @@ function upload(file: File, name?: string): void {
}
function setVisibility() {
if (props.channel) {
visibility.value = 'public';
localOnly.value = true; // TODO:
return;
}
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
currentVisibility: visibility.value,
currentVisibility: actualVisibility.value,
isSilenced: $i.isSilenced,
localOnly: localOnly.value,
// false
localOnly: postChannel.value ? false : actualLocalOnly.value,
src: visibilityButton.value,
currentChannel: postChannel.value,
}, {
changeVisibility: v => {
visibility.value = v;
postChannel.value = null;
if (defaultStore.state.rememberNoteVisibility) {
defaultStore.set('visibility', visibility.value);
}
},
changeChannel: channel => {
// computedlocalOnlyvisibility
postChannel.value = channel;
},
}, 'closed');
}
async function toggleLocalOnly() {
if (props.channel) {
visibility.value = 'public';
localOnly.value = true; // TODO:
return;
}
const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo');
if (!localOnly.value && neverShowInfo !== 'true') {
@ -715,7 +719,8 @@ async function post(ev?: MouseEvent) {
text.value.includes('$[scale') ||
text.value.includes('$[position');
if (annoying && visibility.value === 'public') {
// 稿public
if (annoying && actualVisibility.value === 'public' && !postChannel.value) {
const { canceled, result } = await os.actions({
type: 'warning',
text: i18n.ts.thisPostMayBeAnnoying,
@ -744,12 +749,12 @@ async function post(ev?: MouseEvent) {
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : 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,
cw: useCw.value ? cw.value ?? '' : null,
localOnly: localOnly.value,
visibility: visibility.value,
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
localOnly: actualLocalOnly.value,
visibility: actualVisibility.value,
visibleUserIds: actualVisibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
reactionAcceptance: reactionAcceptance.value,
};

View file

@ -9,61 +9,117 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.label, $style.item]">
{{ i18n.ts.visibility }}
</div>
<button key="public" :disabled="isSilenced" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
<button key="public" :disabled="isSilenced" 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.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
</div>
</button>
<button key="home" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
<button key="home" 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.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
</div>
</button>
<button key="followers" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
<button key="followers" 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.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span>
</div>
</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.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span>
<span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
</div>
</button>
<button ref="channelsButton" class="_button" :class="[$style.item, { [$style.active]: currentChannel }]" data-index="5" @click="chooseChannel">
<div :class="$style.channelWrapper" :style="[currentChannel ? {borderLeftColor: `${currentChannel.color}`} : {}]">
<div :class="$style.icon">
<i class="ti ti-device-tv"></i>
</div>
<div :class="$style.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.channel }}</span>
<span :class="$style.itemDescription">
<span v-if="currentChannelName">{{ i18n.t('_visibility.channelSelected', { name: currentChannelName }) }}</span>
<span v-else>{{ i18n.ts._visibility.channelDescription }}</span>
</span>
</div>
</div>
</button>
</div>
</MkModal>
</template>
<script lang="ts" setup>
import { nextTick, shallowRef, ref } from 'vue';
import { nextTick, shallowRef, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
const modal = shallowRef<InstanceType<typeof MkModal>>();
const channelsButton = shallowRef<InstanceType<typeof HTMLButtonElement>>();
const props = withDefaults(defineProps<{
currentVisibility: typeof Misskey.noteVisibilities[number];
isSilenced: boolean;
localOnly: boolean;
src?: HTMLElement;
currentChannel?: Misskey.entities.Channel
}>(), {
});
const emit = defineEmits<{
(ev: 'changeVisibility', v: typeof Misskey.noteVisibilities[number]): void;
(ev: 'changeChannel', v: Misskey.entities.Channel) : void
(ev: 'closed'): void;
}>();
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 misskeyApi('channels/my-favorites', {
limit: 100,
});
channels.value.splice(0, 0, ...res);
}
async function chooseChannel() {
let selectedChannel: Misskey.entities.Channel | null = null;
await os.popupMenu(
channels.value.map(it => ({ type: 'button', text: it.name, action: (_) => selectedChannel = it })),
channelsButton.value,
);
if (selectedChannel) {
emit('changeChannel', selectedChannel);
}
await nextTick();
if (modal.value) modal.value.close();
}
function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
v.value = visibility;
emit('changeVisibility', visibility);
@ -71,6 +127,8 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
if (modal.value) modal.value.close();
});
}
fetchChannels();
</script>
<style lang="scss" module>
@ -154,4 +212,13 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
.itemDescription {
opacity: 0.6;
}
.channelWrapper {
display: flex;
margin-left: -6px;
padding-left: 4px;
border-left-width: 2px;
border-left-style: solid;
border-left-color: transparent;
}
</style>