Skip to content

Commit

Permalink
feat: 投稿フォーム下部の項目をカスタマイズできるように (#139)
Browse files Browse the repository at this point in the history
* change: 投稿フォーム下部の項目の切り離し

* feat: storeに投稿フォーム下部の項目の並び順を保存できるように

* remove: console.log

* add: 設定ページを追加

* add: 翻訳を追加
  • Loading branch information
hideki0403 authored Mar 19, 2024
1 parent 73d8359 commit f760650
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 16 deletions.
8 changes: 8 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5284,6 +5284,14 @@ export interface Locale extends ILocale {
* このお知らせはチャンネルのタイムライン上部に表示されます。最初の1行がタイトルとして表示され、2行目以降はお知らせをタップすることで表示されるようになります。
*/
"channelAnnouncementDescription": string;
/**
* 投稿フォーム
*/
"postForm": string;
/**
* 投稿フォームの下部に表示される項目の並び替えが出来ます。項目をクリックすると削除できます。
*/
"postFormBottomSettingsDescription": string;
"_bubbleGame": {
/**
* 遊び方
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,8 @@ noteDeletationAt: "このノートは{time}に消滅します"
hideActivity: "アクティビティを非公開にする"
hideActivityDescription: "自分のプロフィールのアクティビティ (概要/アクティビティタブ) を他人が見れないようにします。このオプションを有効にしても、自分であればプロフィールのアクティビティタブから引き続き閲覧できます。"
channelAnnouncementDescription: "このお知らせはチャンネルのタイムライン上部に表示されます。最初の1行がタイトルとして表示され、2行目以降はお知らせをタップすることで表示されるようになります。"
postForm: "投稿フォーム"
postFormBottomSettingsDescription: "投稿フォームの下部に表示される項目の並び替えが出来ます。項目をクリックすると削除できます。"

_bubbleGame:
howToPlay: "遊び方"
Expand Down
55 changes: 45 additions & 10 deletions packages/frontend/src/components/MkPostForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<footer :class="$style.footer">
<div :class="$style.footerLeft">
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
<button v-tooltip="i18n.ts.scheduledNoteDelete" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: scheduledNoteDelete }]" @click="toggleScheduledNoteDelete"><i class="ti ti-bomb"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
<template v-for="item in defaultStore.state.postFormActions">
<button v-if="!bottomItemActionDef[item].hide" :key="item" v-tooltip="bottomItemDef[item].title" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: bottomItemActionDef[item].active }]" v-on="bottomItemActionDef[item].action ? { click: bottomItemActionDef[item].action } : {}"><i class="ti" :class="bottomItemDef[item].icon"></i></button>
</template>
</div>
<div :class="$style.footerRight">
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
Expand All @@ -102,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>

<script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, reactive } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
Expand Down Expand Up @@ -132,6 +126,7 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
import { bottomItemDef } from '@/scripts/post-form.js';

const $i = signinRequired();

Expand Down Expand Up @@ -265,6 +260,46 @@ const canPost = computed((): boolean => {
const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags'));

const bottomItemActionDef: Record<keyof typeof bottomItemDef, {
hide?: boolean;
active?: any;
action?: any;
}> = reactive({
attachFile: {
action: chooseFileFrom,
},
poll: {
active: poll,
action: togglePoll,
},
scheduledNoteDelete: {
active: scheduledNoteDelete,
action: toggleScheduledNoteDelete,
},
useCw: {
active: useCw,
action: () => useCw.value = !useCw.value,
},
mention: {
action: insertMention,
},
hashtags: {
active: withHashtags,
action: () => withHashtags.value = !withHashtags.value,
},
plugins: {
hide: postFormActions.length === 0,
action: showActions,
},
emoji: {
action: insertEmoji,
},
addMfmFunction: {
hide: computed(() => !showAddMfmFunction.value),
action: insertMfmFunction,
},
});

watch(text, () => {
checkMissingMention();
}, { immediate: true });
Expand Down
7 changes: 1 addition & 6 deletions packages/frontend/src/pages/settings/general.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
<MkSwitch v-model="disableNoteDrafting">
<template #caption>{{ i18n.ts.disableNoteDraftingDescription }}</template>
{{ i18n.ts.disableNoteDrafting }}
<span class="_beta">{{ i18n.ts.originalFeature }}</span>
</MkSwitch>
<FormLink to="/settings/post-form">{{ i18n.ts.postForm }}</FormLink>
<MkFolder>
<template #label>{{ i18n.ts.pinnedList }}</template>
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
Expand Down Expand Up @@ -326,7 +322,6 @@ const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthO
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const collapseRenotesTrigger = computed(defaultStore.makeGetterSetter('collapseRenotesTrigger'));
const collapseSelfRenotes = computed(defaultStore.makeGetterSetter('collapseSelfRenotes'));
const disableNoteDrafting = computed(defaultStore.makeGetterSetter('disableNoteDrafting'));
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
Expand Down
147 changes: 147 additions & 0 deletions packages/frontend/src/pages/settings/post-form.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<template>
<div class="_gaps_m">
<FormSlot>
<template #label>{{ i18n.ts.postForm }}<span class="_beta">{{ i18n.ts.originalFeature }}</span></template>
<MkContainer :showHeader="false">
<Sortable
v-model="items"
:class="$style.items"
:itemKey="items => items"
:animation="100"
:delay="50"
:delayOnTouchOnly="true"
>
<template #item="{element}">
<button v-tooltip="bottomItemDef[element.type].title" class="_button" :class="$style.item" @click="removeItem(element.type, $event)">
<i class="ti ti-fw" :class="[$style.itemIcon, bottomItemDef[element.type].icon]"></i>
</button>
</template>
</Sortable>
</MkContainer>
</FormSlot>
<div class="_buttons">
<MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton>
<MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
<MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
<div :class="$style.label">{{ i18n.ts.postFormBottomSettingsDescription }}</div>
<MkSwitch v-model="disableNoteDrafting">
<template #caption>{{ i18n.ts.disableNoteDraftingDescription }}</template>
{{ i18n.ts.disableNoteDrafting }}
<span class="_beta">{{ i18n.ts.originalFeature }}</span>
</MkSwitch>
</div>
</template>

<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSlot from '@/components/form/slot.vue';
import MkContainer from '@/components/MkContainer.vue';
import { bottomItemDef } from '@/scripts/post-form.js';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const disableNoteDrafting = computed(defaultStore.makeGetterSetter('disableNoteDrafting'));
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const items = ref(defaultStore.state.postFormActions.map(x => ({
id: Math.random().toString(),
type: x,
})));
async function addItem() {
const currentItems = items.value.map(x => x.type);
const bottomItem = Object.keys(bottomItemDef).filter(k => !currentItems.includes(k));
const { canceled, result: item } = await os.select({
title: i18n.ts.addItem,
items: bottomItem.map(k => ({
value: k, text: bottomItemDef[k].title,
})),
});
if (canceled || item == null) return;
items.value = [...items.value, {
id: Math.random().toString(),
type: item,
}];
}
function getHTMLElement(ev: MouseEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
function removeItem(type: keyof typeof bottomItemDef, ev: MouseEvent) {
const item = bottomItemDef[type];
os.popupMenu([{
type: 'label',
text: item.title,
}, {
text: i18n.ts.remove,
action: () => {
items.value = items.value.filter(x => x.type !== type);
},
}], getHTMLElement(ev));
}
async function save() {
defaultStore.set('postFormActions', items.value.map(x => x.type));
}
async function reset() {
const result = await os.confirm({
type: 'warning',
text: i18n.ts.resetAreYouSure,
});
if (result.canceled) return;
items.value = defaultStore.def.postFormActions.default.map(x => ({
id: Math.random().toString(),
type: x,
}));
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(() => ({
title: i18n.ts.postForm,
icon: 'ti ti-pencil',
}));
</script>

<style lang="scss" module>
.items {
padding: 8px;
flex: 1;
display: grid;
grid-auto-flow: row;
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
grid-auto-rows: 40px;
}
.item {
display: inline-block;
padding: 0;
margin: 0;
font-size: 1em;
width: auto;
height: 100%;
border-radius: 6px;
&:hover {
background: var(--X5);
}
}
.label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
}
</style>
4 changes: 4 additions & 0 deletions packages/frontend/src/router/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ const routes: RouteDef[] = [{
path: '/navbar',
name: 'navbar',
component: page(() => import('@/pages/settings/navbar.vue')),
}, {
path: '/post-form',
name: 'post-form',
component: page(() => import('@/pages/settings/post-form.vue')),
}, {
path: '/statusbar',
name: 'statusbar',
Expand Down
40 changes: 40 additions & 0 deletions packages/frontend/src/scripts/post-form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { i18n } from '@/i18n.js';

export const bottomItemDef = {
attachFile: {
title: i18n.ts.attachFile,
icon: 'ti-photo-plus',
},
poll: {
title: i18n.ts.poll,
icon: 'ti-chart-arrows',
},
scheduledNoteDelete: {
title: i18n.ts.scheduledNoteDelete,
icon: 'ti-bomb',
},
useCw: {
title: i18n.ts.useCw,
icon: 'ti-eye-off',
},
mention: {
title: i18n.ts.mention,
icon: 'ti-at',
},
hashtags: {
title: i18n.ts.hashtags,
icon: 'ti-hash',
},
plugins: {
title: i18n.ts.plugins,
icon: 'ti-plug',
},
emoji: {
title: i18n.ts.emoji,
icon: 'ti-mood-happy',
},
addMfmFunction: {
title: i18n.ts.addMfmFunction,
icon: 'ti-palette',
},
};
14 changes: 14 additions & 0 deletions packages/frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ export const defaultStore = markRaw(new Storage('base', {
'ui',
],
},
postFormActions: {
where: 'deviceAccount',
default: [
'attachFile',
'poll',
'scheduledNoteDelete',
'useCw',
'mention',
'hashtags',
'plugins',
'emoji',
'addMfmFunction',
],
},
visibility: {
where: 'deviceAccount',
default: 'public' as 'public' | 'home' | 'followers' | 'specified',
Expand Down

0 comments on commit f760650

Please sign in to comment.