mirror of
https://github.com/h3poteto/whalebird-desktop
synced 2025-01-29 16:49:24 +01:00
[clean] Remove old compose modal
This commit is contained in:
parent
ebbf2ec15e
commit
5847875586
@ -458,7 +458,7 @@
|
||||
"submit": "Submit"
|
||||
},
|
||||
"receive_drop": {
|
||||
"drop_message": "Drop to Upload to Mastodon"
|
||||
"drop_message": "Drop to Upload a file"
|
||||
},
|
||||
"message": {
|
||||
"account_load_error": "Failed to load accounts",
|
||||
|
@ -949,18 +949,6 @@ const ApplicationMenu = (accountsChange: Array<MenuItemConstructorOptions>, menu
|
||||
...applicationQuitMenu
|
||||
]
|
||||
},
|
||||
{
|
||||
label: i18n.t<string>('main_menu.toot.name'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.t<string>('main_menu.toot.new'),
|
||||
accelerator: 'CmdOrCtrl+N',
|
||||
click: () => {
|
||||
mainWindow!.webContents.send('CmdOrCtrl+N')
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: i18n.t<string>('main_menu.edit.name'),
|
||||
submenu: [
|
||||
|
@ -18,11 +18,10 @@
|
||||
<Detail />
|
||||
</el-aside>
|
||||
<modals></modals>
|
||||
<receive-drop v-show="droppableVisible"></receive-drop>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { defineComponent, computed, ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18next } from 'vue3-i18next'
|
||||
@ -33,28 +32,22 @@ import Compose from './TimelineSpace/Compose.vue'
|
||||
import Modals from './TimelineSpace/Modals.vue'
|
||||
import Detail from './TimelineSpace/Detail.vue'
|
||||
import Mousetrap from 'mousetrap'
|
||||
import ReceiveDrop from './TimelineSpace/ReceiveDrop.vue'
|
||||
import { AccountLoadError } from '@/errors/load'
|
||||
import { TimelineFetchError } from '@/errors/fetch'
|
||||
import { NewTootAttachLength } from '@/errors/validations'
|
||||
import { EventEmitter } from '@/components/event'
|
||||
import { useStore } from '@/store'
|
||||
import { ACTION_TYPES } from '@/store/TimelineSpace'
|
||||
import { MUTATION_TYPES as GLOBAL_HEADER_MUTATION } from '@/store/GlobalHeader'
|
||||
import { MUTATION_TYPES as JUMP_MUTATION } from '@/store/TimelineSpace/Modals/Jump'
|
||||
import { ACTION_TYPES as NEW_TOOT_ACTION } from '@/store/TimelineSpace/Modals/NewToot'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'timeline-space',
|
||||
components: { SideMenu, HeaderMenu, Modals, Contents, ReceiveDrop, Compose, Detail },
|
||||
components: { SideMenu, HeaderMenu, Modals, Contents, Compose, Detail },
|
||||
setup() {
|
||||
const space = 'TimelineSpace'
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const i18n = useI18next()
|
||||
|
||||
const dropTarget = ref<any>(null)
|
||||
const droppableVisible = ref<boolean>(false)
|
||||
const contentsRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const loading = computed(() => store.state.TimelineSpace.loading)
|
||||
@ -64,20 +57,11 @@ export default defineComponent({
|
||||
await initialize().finally(() => {
|
||||
store.commit(`GlobalHeader/${GLOBAL_HEADER_MUTATION.UPDATE_CHANGING}`, false)
|
||||
})
|
||||
;(window as any).addEventListener('dragenter', onDragEnter)
|
||||
;(window as any).addEventListener('dragleave', onDragLeave)
|
||||
;(window as any).addEventListener('dragover', onDragOver)
|
||||
;(window as any).addEventListener('drop', handleDrop)
|
||||
|
||||
Mousetrap.bind(['command+t', 'ctrl+t'], () => {
|
||||
store.commit(`TimelineSpace/Modals/Jump/${JUMP_MUTATION.CHANGE_MODAL}`, true)
|
||||
})
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
;(window as any).removeEventListener('dragenter', onDragEnter)
|
||||
;(window as any).removeEventListener('dragleave', onDragLeave)
|
||||
;(window as any).removeEventListener('dragover', onDragOver)
|
||||
;(window as any).removeEventListener('drop', handleDrop)
|
||||
})
|
||||
|
||||
const clear = async () => {
|
||||
await store.dispatch(`${space}/${ACTION_TYPES.CLEAR_ACCOUNT}`)
|
||||
@ -106,56 +90,6 @@ export default defineComponent({
|
||||
|
||||
await store.dispatch(`${space}/${ACTION_TYPES.PREPARE_SPACE}`)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
droppableVisible.value = false
|
||||
if (e.dataTransfer?.files.item(0) === null || e.dataTransfer?.files.item(0) === undefined) {
|
||||
return false
|
||||
}
|
||||
const file = e.dataTransfer?.files.item(0)
|
||||
if (file === null || (!file.type.includes('image') && !file.type.includes('video'))) {
|
||||
ElMessage({
|
||||
message: i18n.t('validation.new_toot.attach_image'),
|
||||
type: 'error'
|
||||
})
|
||||
return false
|
||||
}
|
||||
store.dispatch(`TimelineSpace/Modals/NewToot/${NEW_TOOT_ACTION.OPEN_MODAL}`)
|
||||
store
|
||||
.dispatch(`TimelineSpace/Modals/NewToot/${NEW_TOOT_ACTION.UPLOAD_IMAGE}`, file)
|
||||
.then(() => {
|
||||
EventEmitter.emit('image-uploaded')
|
||||
})
|
||||
.catch(err => {
|
||||
if (err instanceof NewTootAttachLength) {
|
||||
ElMessage({
|
||||
message: i18n.t('validation.new_toot.attach_length', { max: 4 }),
|
||||
type: 'error'
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
message: i18n.t('message.attach_error'),
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
if (e.dataTransfer && e.dataTransfer.types.indexOf('Files') >= 0) {
|
||||
dropTarget.value = e.target
|
||||
droppableVisible.value = true
|
||||
}
|
||||
}
|
||||
const onDragLeave = (e: DragEvent) => {
|
||||
if (e.target === dropTarget.value) {
|
||||
droppableVisible.value = false
|
||||
}
|
||||
}
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const composeResized = (event: { width: number; height: number }) => {
|
||||
if (contentsRef.value) {
|
||||
@ -165,7 +99,6 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
loading,
|
||||
droppableVisible,
|
||||
composeResized,
|
||||
contentsRef,
|
||||
detail
|
||||
|
@ -96,22 +96,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<receive-drop v-if="droppableVisible"></receive-drop>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, computed, ref, onMounted, watch } from 'vue'
|
||||
import { defineComponent, reactive, computed, ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import generator, { Entity, MegalodonInterface } from 'megalodon'
|
||||
import emojiDefault from 'emoji-mart-vue-fast/data/all.json'
|
||||
import { Picker, EmojiIndex } from 'emoji-mart-vue-fast/src'
|
||||
import { useI18next } from 'vue3-i18next'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useStore } from '@/store'
|
||||
import { MyWindow } from '~/src/types/global'
|
||||
import { LocalAccount } from '~/src/types/localAccount'
|
||||
import { LocalServer } from '~/src/types/localServer'
|
||||
import visibilityList from '~/src/constants/visibility'
|
||||
import { MUTATION_TYPES } from '@/store/TimelineSpace/Compose'
|
||||
import ReceiveDrop from './ReceiveDrop.vue'
|
||||
|
||||
type Expire = {
|
||||
label: string
|
||||
@ -120,7 +123,7 @@ type Expire = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Compose',
|
||||
components: { Picker },
|
||||
components: { Picker, ReceiveDrop },
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
@ -196,6 +199,9 @@ export default defineComponent({
|
||||
const imageRef = ref<any>(null)
|
||||
const statusRef = ref<any>(null)
|
||||
|
||||
const dropTarget = ref<any>(null)
|
||||
const droppableVisible = ref<boolean>(false)
|
||||
|
||||
onMounted(async () => {
|
||||
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
|
||||
const c = generator(s.sns, s.baseURL, a.accessToken, userAgent.value)
|
||||
@ -221,6 +227,17 @@ export default defineComponent({
|
||||
imageUrl: e.image
|
||||
}))
|
||||
emojiData.value = new EmojiIndex(emojiDefault, { custom: customEmojis })
|
||||
;(window as any).addEventListener('dragenter', onDragEnter)
|
||||
;(window as any).addEventListener('dragleave', onDragLeave)
|
||||
;(window as any).addEventListener('dragover', onDragOver)
|
||||
;(window as any).addEventListener('drop', handleDrop)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
;(window as any).removeEventListener('dragenter', onDragEnter)
|
||||
;(window as any).removeEventListener('dragleave', onDragLeave)
|
||||
;(window as any).removeEventListener('dragover', onDragOver)
|
||||
;(window as any).removeEventListener('drop', handleDrop)
|
||||
})
|
||||
|
||||
watch(inReplyTo, current => {
|
||||
@ -349,6 +366,45 @@ export default defineComponent({
|
||||
poll.options = poll.options.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
droppableVisible.value = false
|
||||
if (e.dataTransfer?.files.item(0) === null || e.dataTransfer?.files.item(0) === undefined) {
|
||||
return false
|
||||
}
|
||||
const file = e.dataTransfer?.files.item(0)
|
||||
if (file === null || (!file.type.includes('image') && !file.type.includes('video'))) {
|
||||
ElMessage({
|
||||
message: i18n.t('validation.new_toot.attach_image'),
|
||||
type: 'error'
|
||||
})
|
||||
return false
|
||||
}
|
||||
uploadImage(file).catch(err => {
|
||||
console.error(err)
|
||||
ElMessage({
|
||||
message: i18n.t('message.attach_error'),
|
||||
type: 'error'
|
||||
})
|
||||
})
|
||||
return false
|
||||
}
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
if (e.dataTransfer && e.dataTransfer.types.indexOf('Files') >= 0) {
|
||||
dropTarget.value = e.target
|
||||
droppableVisible.value = true
|
||||
}
|
||||
}
|
||||
const onDragLeave = (e: DragEvent) => {
|
||||
if (e.target === dropTarget.value) {
|
||||
droppableVisible.value = false
|
||||
}
|
||||
}
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
post,
|
||||
@ -371,7 +427,8 @@ export default defineComponent({
|
||||
togglePoll,
|
||||
expiresList,
|
||||
addPollOption,
|
||||
removePollOption
|
||||
removePollOption,
|
||||
droppableVisible
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -5,9 +5,6 @@
|
||||
</div>
|
||||
<div class="tools">
|
||||
<img src="../../assets/images/loading-spinner-wide.svg" v-show="loading" class="header-loading" />
|
||||
<el-button class="action" link :title="$t('header_menu.new_toot')" @click="openNewTootModal">
|
||||
<font-awesome-icon :icon="['far', 'pen-to-square']" />
|
||||
</el-button>
|
||||
<el-button v-show="reloadable()" link class="action" :title="$t('header_menu.reload')" @click="reload">
|
||||
<font-awesome-icon icon="rotate" />
|
||||
</el-button>
|
||||
@ -42,7 +39,6 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18next } from 'vue3-i18next'
|
||||
import { useStore } from '@/store'
|
||||
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/HeaderMenu'
|
||||
import { ACTION_TYPES as NEW_TOOT_ACTION } from '@/store/TimelineSpace/Modals/NewToot'
|
||||
import { MUTATION_TYPES as HOME_MUTATION } from '@/store/TimelineSpace/Contents/Home'
|
||||
|
||||
export default defineComponent({
|
||||
@ -123,9 +119,7 @@ export default defineComponent({
|
||||
break
|
||||
}
|
||||
}
|
||||
const openNewTootModal = () => {
|
||||
store.dispatch(`TimelineSpace/Modals/NewToot/${NEW_TOOT_ACTION.OPEN_MODAL}`)
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
switch (route.name) {
|
||||
case 'favourites':
|
||||
@ -185,7 +179,6 @@ export default defineComponent({
|
||||
return {
|
||||
title,
|
||||
loading,
|
||||
openNewTootModal,
|
||||
reloadable,
|
||||
reload,
|
||||
TLOption,
|
||||
|
@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<new-toot v-if="newTootModal"></new-toot>
|
||||
<jump v-if="jumpModal"></jump>
|
||||
<image-viewer></image-viewer>
|
||||
<list-membership v-if="listMembershipModal"></list-membership>
|
||||
@ -14,7 +13,6 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import NewToot from './Modals/NewToot.vue'
|
||||
import Jump from './Modals/Jump.vue'
|
||||
import ImageViewer from './Modals/ImageViewer.vue'
|
||||
import ListMembership from './Modals/ListMembership.vue'
|
||||
@ -26,7 +24,6 @@ import Report from './Modals/Report.vue'
|
||||
export default defineComponent({
|
||||
name: 'modals',
|
||||
components: {
|
||||
NewToot,
|
||||
Jump,
|
||||
ImageViewer,
|
||||
ListMembership,
|
||||
@ -37,7 +34,6 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const store = useStore()
|
||||
const newTootModal = computed(() => store.state.TimelineSpace.Modals.NewToot.modalOpen)
|
||||
const jumpModal = computed(() => store.state.TimelineSpace.Modals.Jump.modalOpen)
|
||||
const reportModal = computed(() => store.state.TimelineSpace.Modals.Report.modalOpen)
|
||||
const muteConfirmModal = computed(() => store.state.TimelineSpace.Modals.MuteConfirm.modalOpen)
|
||||
@ -45,7 +41,6 @@ export default defineComponent({
|
||||
const listMembershipModal = computed(() => store.state.TimelineSpace.Modals.ListMembership.modalOpen)
|
||||
|
||||
return {
|
||||
newTootModal,
|
||||
jumpModal,
|
||||
reportModal,
|
||||
muteConfirmModal,
|
||||
|
@ -1,704 +0,0 @@
|
||||
<template>
|
||||
<div class="new-toot">
|
||||
<el-dialog
|
||||
:title="$t('modals.new_toot.title')"
|
||||
:model-value="newTootModal"
|
||||
@update:model-value="newTootModal = $event"
|
||||
:before-close="closeConfirm"
|
||||
width="600px"
|
||||
custom-class="new-toot-modal"
|
||||
v-if="newTootModal"
|
||||
>
|
||||
<el-form v-on:submit.prevent="toot" role="form">
|
||||
<Quote :message="quoteToMessage" :displayNameStyle="displayNameStyle" v-if="quoteToMessage !== null" ref="quoteRef"></Quote>
|
||||
<div class="spoiler" v-if="showContentWarning" ref="spoilerRef">
|
||||
<div class="el-input">
|
||||
<input type="text" class="el-input__inner" :placeholder="$t('modals.new_toot.cw')" v-model="spoilerText" />
|
||||
</div>
|
||||
</div>
|
||||
<Status
|
||||
:modelValue="statusText"
|
||||
@update:modelValue="statusText = $event"
|
||||
:opened="newTootModal"
|
||||
:fixCursorPos="hashtagInserting"
|
||||
:height="statusHeight"
|
||||
@paste="onPaste"
|
||||
@toot="toot"
|
||||
ref="statusRef"
|
||||
v-if="newTootModal"
|
||||
/>
|
||||
</el-form>
|
||||
<Poll
|
||||
v-if="openPoll"
|
||||
v-model:polls="polls"
|
||||
:expire="pollExpire"
|
||||
@update:expire="updatePollExpire"
|
||||
@addPoll="addPoll"
|
||||
@removePoll="removePoll"
|
||||
ref="pollRef"
|
||||
></Poll>
|
||||
<div class="preview" ref="previewRef">
|
||||
<div class="image-wrapper" v-for="media in attachedMedias" v-bind:key="media.id">
|
||||
<img :src="media.preview_url" class="preview-image" />
|
||||
<el-button class="remove-image" link @click="removeAttachment(media)"><font-awesome-icon icon="circle-xmark" /></el-button>
|
||||
<textarea
|
||||
maxlength="420"
|
||||
class="image-description"
|
||||
:placeholder="$t('modals.new_toot.description')"
|
||||
:value="mediaDescriptions[media.id]"
|
||||
@input="updateDescription(media.id, $event.target.value)"
|
||||
role="textbox"
|
||||
contenteditable="true"
|
||||
aria-multiline="true"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<div class="upload-image">
|
||||
<el-button size="default" link :title="$t('modals.new_toot.footer.add_image')" @click="selectImage">
|
||||
<font-awesome-icon icon="camera" />
|
||||
</el-button>
|
||||
<input name="image" type="file" class="image-input" ref="imageRef" @change="onChangeImage" />
|
||||
</div>
|
||||
<div class="poll">
|
||||
<el-button size="default" link :title="$t('modals.new_toot.footer.poll')" @click="togglePollForm">
|
||||
<font-awesome-icon icon="square-poll-horizontal" />
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="privacy">
|
||||
<el-dropdown trigger="click" @command="changeVisibility">
|
||||
<el-button size="default" link :title="$t('modals.new_toot.footer.change_visibility')">
|
||||
<font-awesome-icon :icon="visibilityIcon" />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="visibilityList.Public.value">
|
||||
<font-awesome-icon icon="globe" class="privacy-icon" />
|
||||
{{ $t(visibilityList.Public.name) }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="visibilityList.Unlisted.value">
|
||||
<font-awesome-icon icon="unlock" class="privacy-icon" />
|
||||
{{ $t(visibilityList.Unlisted.name) }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="visibilityList.Private.value">
|
||||
<font-awesome-icon icon="lock" class="privacy-icon" />
|
||||
{{ $t(visibilityList.Private.name) }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="visibilityList.Direct.value">
|
||||
<font-awesome-icon icon="envelope" class="privacy-icon" size="sm" />
|
||||
{{ $t(visibilityList.Direct.name) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="sensitive" v-show="attachedMedias.length > 0">
|
||||
<el-button
|
||||
size="default"
|
||||
link
|
||||
@click="changeSensitive"
|
||||
:title="$t('modals.new_toot.footer.change_sensitive')"
|
||||
:aria-pressed="sensitive"
|
||||
>
|
||||
<font-awesome-icon icon="eye-slash" v-show="!sensitive" />
|
||||
<font-awesome-icon icon="eye" v-show="sensitive" />
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="content-warning">
|
||||
<el-button
|
||||
size="default"
|
||||
link
|
||||
@click="toggleContentWarning()"
|
||||
:title="$t('modals.new_toot.footer.add_cw')"
|
||||
:class="showContentWarning ? '' : 'clickable'"
|
||||
:aria-pressed="showContentWarning"
|
||||
>
|
||||
<font-awesome-icon icon="eye-slash" />
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="pined-hashtag">
|
||||
<el-button
|
||||
size="default"
|
||||
link
|
||||
@click="pinedHashtag = !pinedHashtag"
|
||||
:title="$t('modals.new_toot.footer.pined_hashtag')"
|
||||
:class="pinedHashtag ? '' : 'clickable'"
|
||||
:aria-pressed="pinedHashtag"
|
||||
>
|
||||
<font-awesome-icon icon="hashtag" />
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="info">
|
||||
<img src="../../../assets/images/loading-spinner-wide.svg" v-show="loading" class="loading" />
|
||||
<span class="text-count">{{ tootMax - statusText.length }}</span>
|
||||
|
||||
<el-button class="toot-action" @click="closeConfirm(close)">{{ $t('modals.new_toot.cancel') }}</el-button>
|
||||
<el-button class="toot-action" type="primary" @click="toot" :loading="blockSubmit">{{ $t('modals.new_toot.toot') }}</el-button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</template>
|
||||
<resize-observer @notify="handleResize" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, reactive, computed, onMounted, ComponentPublicInstance, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { useI18next } from 'vue3-i18next'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Entity } from 'megalodon'
|
||||
import { useStore } from '@/store'
|
||||
import Visibility from '~/src/constants/visibility'
|
||||
import Status from './NewToot/Status.vue'
|
||||
import Poll from './NewToot/Poll.vue'
|
||||
import Quote from './NewToot/Quote.vue'
|
||||
import { NewTootTootLength, NewTootAttachLength, NewTootModalOpen, NewTootBlockSubmit, NewTootPollInvalid } from '@/errors/validations'
|
||||
import { EventEmitter } from '@/components/event'
|
||||
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Modals/NewToot'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'new-toot',
|
||||
components: {
|
||||
Status,
|
||||
Poll,
|
||||
Quote
|
||||
},
|
||||
setup() {
|
||||
const space = 'TimelineSpace/Modals/NewToot'
|
||||
const store = useStore()
|
||||
const i18n = useI18next()
|
||||
const visibilityList = Visibility
|
||||
|
||||
const enableResizing = ref<boolean>(true)
|
||||
const statusText = ref<string>('')
|
||||
const spoilerText = ref<string>('')
|
||||
const showContentWarning = ref<boolean>(false)
|
||||
const openPoll = ref<boolean>(false)
|
||||
const polls = ref<Array<string>>([])
|
||||
const pollExpire = reactive({
|
||||
label: i18n.t('modals.new_toot.poll.expires.1_day'),
|
||||
value: 3600 * 24
|
||||
})
|
||||
const statusHeight = ref<number>(240)
|
||||
const previewRef = ref<HTMLElement>()
|
||||
const imageRef = ref<HTMLInputElement>()
|
||||
const pollRef = ref<ComponentPublicInstance>()
|
||||
const spoilerRef = ref<HTMLElement>()
|
||||
const quoteRef = ref<ComponentPublicInstance>()
|
||||
const statusRef = ref<InstanceType<typeof Status>>()
|
||||
|
||||
const quoteToMessage = computed(() => store.state.TimelineSpace.Modals.NewToot.quoteToMessage)
|
||||
const attachedMedias = computed(() => store.state.TimelineSpace.Modals.NewToot.attachedMedias)
|
||||
const mediaDescriptions = computed(() => store.state.TimelineSpace.Modals.NewToot.mediaDescriptions)
|
||||
const blockSubmit = computed(() => store.state.TimelineSpace.Modals.NewToot.blockSubmit)
|
||||
const sensitive = computed(() => store.state.TimelineSpace.Modals.NewToot.sensitive)
|
||||
const initialStatus = computed(() => store.state.TimelineSpace.Modals.NewToot.initialStatus)
|
||||
const initialSpoiler = computed(() => store.state.TimelineSpace.Modals.NewToot.initialSpoiler)
|
||||
const visibilityIcon = computed(() => {
|
||||
switch (store.state.TimelineSpace.Modals.NewToot.visibility) {
|
||||
case Visibility.Public.value:
|
||||
return 'globe'
|
||||
case Visibility.Unlisted.value:
|
||||
return 'unlock'
|
||||
case Visibility.Private.value:
|
||||
return 'lock'
|
||||
case Visibility.Direct.value:
|
||||
return 'envelope'
|
||||
default:
|
||||
return 'globe'
|
||||
}
|
||||
})
|
||||
const loading = computed(() => store.state.TimelineSpace.Modals.NewToot.loading)
|
||||
const tootMax = computed(() => store.state.TimelineSpace.tootMax)
|
||||
const displayNameStyle = computed(() => store.state.App.displayNameStyle)
|
||||
const hashtagInserting = computed(() => store.getters[`${space}/hashtagInserting`])
|
||||
const newTootModal = computed({
|
||||
get: () => store.state.TimelineSpace.Modals.NewToot.modalOpen,
|
||||
set: (value: boolean) => {
|
||||
if (value) {
|
||||
store.dispatch(`${space}/${ACTION_TYPES.OPEN_MODAL}`)
|
||||
} else {
|
||||
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_MODAL}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
const pinedHashtag = computed({
|
||||
get: () => store.state.TimelineSpace.Modals.NewToot.pinedHashtag,
|
||||
set: (value: boolean) => store.commit(`${space}/${MUTATION_TYPES.CHANGE_PINED_HASHTAG}`, value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch(`${space}/${ACTION_TYPES.SETUP_LOADING}`)
|
||||
EventEmitter.on('image-uploaded', () => {
|
||||
if (previewRef.value) {
|
||||
statusHeight.value = statusHeight.value - previewRef.value.offsetHeight
|
||||
}
|
||||
})
|
||||
|
||||
showContentWarning.value = initialSpoiler.value.length > 0
|
||||
statusText.value = initialStatus.value
|
||||
spoilerText.value = initialSpoiler.value
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
store.dispatch(`${space}/${ACTION_TYPES.TEARDOWN_LOADING}`)
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
store.dispatch(`${space}/${ACTION_TYPES.RESET_MEDIA_COUNT}`)
|
||||
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_MODAL}`)
|
||||
}
|
||||
const toot = async () => {
|
||||
const form = {
|
||||
status: statusText.value,
|
||||
spoiler: spoilerText.value,
|
||||
polls: polls.value,
|
||||
pollExpireSeconds: pollExpire.value
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await store.dispatch(`${space}/${ACTION_TYPES.POST_TOOT}`, form)
|
||||
store.dispatch(`${space}/${ACTION_TYPES.UPDATE_HASHTAGS}`, status.tags)
|
||||
close()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (err instanceof NewTootTootLength) {
|
||||
ElMessage({
|
||||
message: i18n.t('validation.new_toot.toot_length', {
|
||||
min: 1,
|
||||
max: tootMax.value
|
||||
}),
|
||||
type: 'error'
|
||||
})
|
||||
} else if (err instanceof NewTootAttachLength) {
|
||||
ElMessage({
|
||||
message: i18n.t('validation.new_toot.attach_length', { max: 4 }),
|
||||
type: 'error'
|
||||
})
|
||||
} else if (err instanceof NewTootPollInvalid) {
|
||||
ElMessage({
|
||||
message: i18n.t('validation.new_toot.poll_invalid'),
|
||||
type: 'error'
|
||||
})
|
||||
} else if (err instanceof NewTootModalOpen || err instanceof NewTootBlockSubmit) {
|
||||
// Nothing
|
||||
} else {
|
||||
ElMessage({
|
||||
message: i18n.t('message.toot_error'),
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const selectImage = () => {
|
||||
imageRef!.value!.click()
|
||||
}
|
||||
const updateImage = (file: File) => {
|
||||
store
|
||||
.dispatch(`${space}/${ACTION_TYPES.UPLOAD_IMAGE}`, file)
|
||||
.then(() => {
|
||||
enableResizing.value = false
|
||||
nextTick(() => {
|
||||
if (attachedMedias.value.length === 1 && previewRef.value) {
|
||||
statusHeight.value = statusHeight.value - previewRef.value.offsetHeight
|
||||
}
|
||||
enableResizing.value = true
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
if (err instanceof NewTootAttachLength) {
|
||||
ElMessage({
|
||||
message: i18n.t('validation.new_toot.attach_length', { max: 4 }),
|
||||
type: 'error'
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
message: i18n.t('message.attach_error'),
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const onChangeImage = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const file = target.files?.item(0)
|
||||
if (file === null || file === undefined) {
|
||||
return
|
||||
}
|
||||
if (!file.type.includes('image') && !file.type.includes('video')) {
|
||||
ElMessage({
|
||||
message: i18n.t('validation.new_toot.attach_image'),
|
||||
type: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
updateImage(file)
|
||||
}
|
||||
const onPaste = (e: Event) => {
|
||||
const mimeTypes = (window as any).clipboard.availableFormats().filter(t => t.startsWith('image'))
|
||||
if (mimeTypes.length === 0) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
const image = (window as any).clipboard.readImage()
|
||||
let data: any
|
||||
if (/^image\/jpe?g$/.test(mimeTypes[0])) {
|
||||
data = image.toJPEG(100)
|
||||
} else {
|
||||
data = image.toPNG()
|
||||
}
|
||||
const file = new File([data], 'clipboard.picture', { type: mimeTypes[0] })
|
||||
updateImage(file)
|
||||
}
|
||||
const removeAttachment = (media: Entity.Attachment) => {
|
||||
const previousHeight = previewRef!.value!.offsetHeight
|
||||
store.dispatch(`${space}/${ACTION_TYPES.REMOVE_MEDIA}`, media).then(() => {
|
||||
enableResizing.value = false
|
||||
nextTick(() => {
|
||||
if (attachedMedias.value.length === 0) {
|
||||
statusHeight.value = statusHeight.value + previousHeight
|
||||
}
|
||||
enableResizing.value = true
|
||||
})
|
||||
})
|
||||
}
|
||||
const changeVisibility = (level: number) => {
|
||||
store.commit(`${space}/${MUTATION_TYPES.CHANGE_VISIBILITY_VALUE}`, level)
|
||||
}
|
||||
const changeSensitive = () => {
|
||||
store.commit(`${space}/${MUTATION_TYPES.CHANGE_SENSITIVE}`, !sensitive.value)
|
||||
}
|
||||
const closeConfirm = (done: Function) => {
|
||||
if (!newTootModal.value) return
|
||||
if (statusRef.value?.suggestOpened) return
|
||||
if (statusText.value.length === 0) {
|
||||
done()
|
||||
} else {
|
||||
ElMessageBox.confirm(i18n.t('modals.new_toot.close_confirm'), {
|
||||
confirmButtonText: i18n.t('modals.new_toot.close_confirm_ok'),
|
||||
cancelButtonText: i18n.t('modals.new_toot.close_confirm_cancel')
|
||||
})
|
||||
.then(_ => {
|
||||
done()
|
||||
})
|
||||
.catch(_ => {})
|
||||
}
|
||||
}
|
||||
const updateDescription = (id: number, value: string) => {
|
||||
store.commit(`${space}/${MUTATION_TYPES.UPDATE_MEDIA_DESCRIPTION}`, { id: id, description: value })
|
||||
}
|
||||
const togglePollForm = () => {
|
||||
const previousHeight = pollRef.value ? pollRef.value.$el.offsetHeight : 0
|
||||
const toggle = () => {
|
||||
openPoll.value = !openPoll.value
|
||||
if (openPoll.value) {
|
||||
polls.value = ['', '']
|
||||
} else {
|
||||
polls.value = []
|
||||
}
|
||||
}
|
||||
enableResizing.value = false
|
||||
toggle()
|
||||
nextTick(() => {
|
||||
if (openPoll.value) {
|
||||
const currentHeight = pollRef.value ? pollRef.value.$el.offsetHeight : 0
|
||||
statusHeight.value = statusHeight.value - currentHeight
|
||||
} else {
|
||||
statusHeight.value = statusHeight.value + previousHeight
|
||||
}
|
||||
enableResizing.value = true
|
||||
})
|
||||
}
|
||||
const addPoll = () => {
|
||||
enableResizing.value = false
|
||||
polls.value.push('')
|
||||
nextTick(() => {
|
||||
enableResizing.value = true
|
||||
})
|
||||
}
|
||||
const removePoll = (id: number) => {
|
||||
enableResizing.value = false
|
||||
polls.value.splice(id, 1)
|
||||
nextTick(() => {
|
||||
enableResizing.value = true
|
||||
})
|
||||
}
|
||||
const updatePollExpire = newExpire => {
|
||||
pollExpire.label = newExpire.label
|
||||
pollExpire.value = newExpire.value
|
||||
}
|
||||
|
||||
const toggleContentWarning = () => {
|
||||
const previousHeight = spoilerRef.value ? spoilerRef.value.offsetHeight : 0
|
||||
enableResizing.value = false
|
||||
showContentWarning.value = !showContentWarning.value
|
||||
nextTick(() => {
|
||||
if (showContentWarning.value) {
|
||||
if (spoilerRef.value) {
|
||||
statusHeight.value = statusHeight.value - spoilerRef.value.offsetHeight
|
||||
}
|
||||
} else {
|
||||
statusHeight.value = statusHeight.value + previousHeight
|
||||
}
|
||||
enableResizing.value = true
|
||||
})
|
||||
}
|
||||
const handleResize = (event: { width: number; height: number }) => {
|
||||
if (!enableResizing.value) return
|
||||
const dialog = document.getElementsByClassName('new-toot-modal').item(0) as HTMLElement
|
||||
if (!dialog) return
|
||||
const dialogStyle = window.getComputedStyle(dialog, null)
|
||||
// Ignore when the modal height already reach window height.
|
||||
const marginTop = dialogStyle.marginTop
|
||||
const limitHeight = document.documentElement.clientHeight - parseInt(marginTop) - 80
|
||||
if (dialog.offsetHeight >= limitHeight) {
|
||||
return
|
||||
}
|
||||
const pollHeight = pollRef.value ? pollRef.value.$el.offsetHeight : 0
|
||||
const spoilerHeight = spoilerRef.value ? spoilerRef.value.offsetHeight : 0
|
||||
const quoteHeight = quoteRef.value ? quoteRef.value.$el.offsetHeight : 0
|
||||
const previewHeight = previewRef.value ? previewRef.value.offsetHeight : 0
|
||||
const headerHeight = 54
|
||||
const footerHeight = 66
|
||||
statusHeight.value = event.height - footerHeight - headerHeight - previewHeight - pollHeight - spoilerHeight - quoteHeight
|
||||
}
|
||||
|
||||
return {
|
||||
visibilityList,
|
||||
statusText,
|
||||
spoilerText,
|
||||
showContentWarning,
|
||||
openPoll,
|
||||
polls,
|
||||
pollExpire,
|
||||
updatePollExpire,
|
||||
statusHeight,
|
||||
// DOM refs
|
||||
previewRef,
|
||||
imageRef,
|
||||
pollRef,
|
||||
spoilerRef,
|
||||
quoteRef,
|
||||
statusRef,
|
||||
// computed
|
||||
quoteToMessage,
|
||||
attachedMedias,
|
||||
mediaDescriptions,
|
||||
blockSubmit,
|
||||
sensitive,
|
||||
visibilityIcon,
|
||||
loading,
|
||||
tootMax,
|
||||
displayNameStyle,
|
||||
hashtagInserting,
|
||||
newTootModal,
|
||||
pinedHashtag,
|
||||
// methods
|
||||
close,
|
||||
toot,
|
||||
selectImage,
|
||||
onChangeImage,
|
||||
onPaste,
|
||||
removeAttachment,
|
||||
changeVisibility,
|
||||
changeSensitive,
|
||||
closeConfirm,
|
||||
updateDescription,
|
||||
togglePollForm,
|
||||
addPoll,
|
||||
removePoll,
|
||||
toggleContentWarning,
|
||||
handleResize
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.new-toot :deep() {
|
||||
.new-toot-modal {
|
||||
background-color: var(--theme-selected-background-color);
|
||||
overflow: hidden;
|
||||
resize: both;
|
||||
padding-bottom: 20px;
|
||||
max-height: calc(100% - 15vh - 80px);
|
||||
max-width: 95%;
|
||||
|
||||
.el-dialog__header {
|
||||
background-color: #4a5664;
|
||||
margin-right: 0;
|
||||
|
||||
.el-dialog__title {
|
||||
color: #ebeef5;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0;
|
||||
|
||||
.el-input__wrapper {
|
||||
background-color: var(--theme-background-color);
|
||||
border: 1px solid var(--theme-border-color);
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
background-color: var(--theme-background-color);
|
||||
color: var(--theme-primary-color);
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
box-sizing: border-box;
|
||||
padding: 4px 0;
|
||||
background-color: #4a5664;
|
||||
|
||||
input {
|
||||
border-radius: 0;
|
||||
|
||||
&::placeholder {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-width: 10%;
|
||||
height: 150px;
|
||||
margin: 4px;
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-description {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0, rgba(0, 0, 0, 0.35) 80%, transparent);
|
||||
font-size: var(--font-base-size);
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
resize: none;
|
||||
overflow: scroll;
|
||||
|
||||
&::placeholder {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-image {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
|
||||
.fa-icon {
|
||||
font-size: 0.9rem;
|
||||
width: auto;
|
||||
height: 1em;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
background-color: var(--theme-selected-background-color);
|
||||
font-size: var(--base-font-size);
|
||||
padding-bottom: 0;
|
||||
|
||||
.upload-image {
|
||||
text-align: left;
|
||||
float: left;
|
||||
|
||||
.image-input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.poll {
|
||||
float: left;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.privacy {
|
||||
float: left;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.sensitive {
|
||||
float: left;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.content-warning {
|
||||
float: left;
|
||||
margin-left: 8px;
|
||||
|
||||
.cw-text {
|
||||
font-weight: 800;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.pined-hashtag {
|
||||
float: left;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
.loading {
|
||||
width: 18px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.text-count {
|
||||
padding-right: 10px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.toot-action {
|
||||
font-size: var(--base-font-size);
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.privacy-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
@ -1,132 +0,0 @@
|
||||
<template>
|
||||
<div class="poll">
|
||||
<ul class="poll-list">
|
||||
<li class="poll-option" v-for="(option, id) in polls" v-bind:key="id">
|
||||
<el-radio :disabled="true" :label="id">
|
||||
<el-input :placeholder="`choice ${id}`" :modelValue="option" @input="polls[id] = $event" size="small"></el-input>
|
||||
<el-button class="remove-poll" link size="small" @click="removePoll(id)"><font-awesome-icon icon="xmark" /></el-button>
|
||||
</el-radio>
|
||||
</li>
|
||||
</ul>
|
||||
<el-button class="add-poll" type="info" size="small" @click="addPoll" plain>{{ $t('modals.new_toot.poll.add_choice') }}</el-button>
|
||||
<el-select :model-value="expire" size="small" value-key="value" @change="updateExpire">
|
||||
<el-option v-for="exp in expiresList" :key="exp.value" :label="exp.label" :value="exp"> </el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, toRefs, reactive, watch } from 'vue'
|
||||
import { useI18next } from 'vue3-i18next'
|
||||
|
||||
type Expire = {
|
||||
label: string
|
||||
value: number
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'poll',
|
||||
props: {
|
||||
polls: {
|
||||
type: Array as PropType<Array<String>>,
|
||||
default: []
|
||||
},
|
||||
expire: {
|
||||
type: Object as PropType<Expire>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['addPoll', 'removePoll', 'update:expire', 'update:polls'],
|
||||
setup(props, ctx) {
|
||||
const i18n = useI18next()
|
||||
const { expire, polls } = toRefs(props)
|
||||
const expiresList = reactive<Array<Expire>>([
|
||||
{
|
||||
label: i18n.t('modals.new_toot.poll.expires.5_minutes'),
|
||||
value: 60 * 5
|
||||
},
|
||||
{
|
||||
label: i18n.t('modals.new_toot.poll.expires.30_minutes'),
|
||||
value: 60 * 30
|
||||
},
|
||||
{
|
||||
label: i18n.t('modals.new_toot.poll.expires.1_hour'),
|
||||
value: 3600
|
||||
},
|
||||
{
|
||||
label: i18n.t('modals.new_toot.poll.expires.6_hours'),
|
||||
value: 3600 * 6
|
||||
},
|
||||
{
|
||||
label: i18n.t('modals.new_toot.poll.expires.1_day'),
|
||||
value: 3600 * 24
|
||||
},
|
||||
{
|
||||
label: i18n.t('modals.new_toot.poll.expires.3_days'),
|
||||
value: 3600 * 24 * 3
|
||||
},
|
||||
{
|
||||
label: i18n.t('modals.new_toot.poll.expires.7_days'),
|
||||
value: 3600 * 24 * 7
|
||||
}
|
||||
])
|
||||
|
||||
const addPoll = () => {
|
||||
ctx.emit('addPoll')
|
||||
}
|
||||
const removePoll = (id: number) => {
|
||||
ctx.emit('removePoll', id)
|
||||
}
|
||||
const updateExpire = newExpire => {
|
||||
ctx.emit('update:expire', newExpire)
|
||||
}
|
||||
|
||||
watch(expire, (newExpire, _old) => {
|
||||
updateExpire(newExpire)
|
||||
})
|
||||
watch(
|
||||
polls,
|
||||
(newPolls, _old) => {
|
||||
ctx.emit('update:polls', newPolls)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
return {
|
||||
polls,
|
||||
expire,
|
||||
expiresList,
|
||||
addPoll,
|
||||
removePoll,
|
||||
updateExpire
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.poll {
|
||||
border-top: 1px solid #ebebeb;
|
||||
|
||||
.poll-list {
|
||||
list-style: none;
|
||||
padding-left: 16px;
|
||||
|
||||
.poll-option {
|
||||
line-height: 38px;
|
||||
|
||||
.remove-poll {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-poll {
|
||||
margin: 0 4px 0 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.poll :deep(.el-input__inner) {
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
@ -1,173 +0,0 @@
|
||||
<template>
|
||||
<div class="quote-target">
|
||||
<div class="icon">
|
||||
<FailoverImg :src="message.account.avatar" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<div class="toot-header">
|
||||
<div class="user">
|
||||
<span class="display-name"><bdi v-html="username(message.account)"></bdi></span>
|
||||
<span class="acct">{{ accountName(message.account) }}</span>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="spoiler" v-html="emojiText(message.spoiler_text, message.emojis)"></div>
|
||||
<div class="content" v-html="emojiText(message.content, message.emojis)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs } from 'vue'
|
||||
import DisplayStyle from '~/src/constants/displayStyle'
|
||||
import FailoverImg from '@/components/atoms/FailoverImg.vue'
|
||||
import emojify from '@/utils/emojify'
|
||||
import { Entity } from 'megalodon'
|
||||
|
||||
export default defineComponent({
|
||||
new: 'quote-target',
|
||||
components: {
|
||||
FailoverImg
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
default: {}
|
||||
},
|
||||
displayNameStyle: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { displayNameStyle } = toRefs(props)
|
||||
const username = (account: Entity.Account) => {
|
||||
switch (displayNameStyle.value) {
|
||||
case DisplayStyle.DisplayNameAndUsername.value:
|
||||
if (account.display_name !== '') {
|
||||
return emojify(account.display_name, account.emojis)
|
||||
} else {
|
||||
return account.acct
|
||||
}
|
||||
case DisplayStyle.DisplayName.value:
|
||||
if (account.display_name !== '') {
|
||||
return emojify(account.display_name, account.emojis)
|
||||
} else {
|
||||
return account.acct
|
||||
}
|
||||
default:
|
||||
return account.acct
|
||||
}
|
||||
}
|
||||
const accountName = (account: Entity.Account) => {
|
||||
switch (displayNameStyle.value) {
|
||||
case DisplayStyle.DisplayNameAndUsername.value:
|
||||
return `@${account.acct}`
|
||||
case DisplayStyle.DisplayName.value:
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const emojiText = (content: string, emojis: Array<Entity.Emoji>) => {
|
||||
return emojify(content, emojis)
|
||||
}
|
||||
|
||||
return {
|
||||
username,
|
||||
accountName,
|
||||
emojiText
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quote-target {
|
||||
background-color: var(--theme-background-color);
|
||||
padding: 8px 12px;
|
||||
|
||||
.icon {
|
||||
float: left;
|
||||
|
||||
img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
margin: 0 8px 0 8px;
|
||||
float: left;
|
||||
width: calc(100% - 52px);
|
||||
|
||||
.content-wrapper {
|
||||
font-size: var(--base-font-size);
|
||||
color: var(--theme-primary-color);
|
||||
|
||||
blockquote {
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid #9baec8;
|
||||
color: #9baec8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: var(--toot-padding) 0;
|
||||
word-wrap: break-word;
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.content p {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper :deep(.emojione) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toot-header {
|
||||
.user {
|
||||
float: left;
|
||||
font-size: var(--base-font-size);
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.display-name {
|
||||
font-weight: 800;
|
||||
color: var(--theme-primary-color);
|
||||
}
|
||||
|
||||
.display-name :deep(.emojione) {
|
||||
max-width: 14px;
|
||||
max-height: 14px;
|
||||
}
|
||||
|
||||
.acct {
|
||||
font-weight: normal;
|
||||
color: var(--theme-secondary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
margin: 8px 0;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,397 +0,0 @@
|
||||
<template>
|
||||
<div class="status">
|
||||
<textarea
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', $event.target?.value)"
|
||||
ref="statusRef"
|
||||
@paste="$emit('paste', $event)"
|
||||
v-on:input="startSuggest"
|
||||
:placeholder="$t('modals.new_toot.status')"
|
||||
role="textbox"
|
||||
contenteditable="true"
|
||||
aria-multiline="true"
|
||||
:style="`height: ${height}px`"
|
||||
v-focus
|
||||
autofocus
|
||||
>
|
||||
</textarea>
|
||||
<el-popover
|
||||
placement="bottom-start"
|
||||
width="300"
|
||||
trigger="manual"
|
||||
popper-class="suggest-popper"
|
||||
:popper-options="popperOptions()"
|
||||
ref="suggestRef"
|
||||
v-model:visible="suggestOpened"
|
||||
>
|
||||
<ul class="suggest-list">
|
||||
<li
|
||||
v-for="(item, index) in filteredSuggestion"
|
||||
:key="index"
|
||||
@click="insertItem(item)"
|
||||
@mouseover="suggestHighlight(index)"
|
||||
:class="{ highlighted: highlightedIndex === index }"
|
||||
>
|
||||
<span v-if="item.image">
|
||||
<img :src="item.image" class="icon" />
|
||||
</span>
|
||||
<span v-if="item.code">
|
||||
{{ item.code }}
|
||||
</span>
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<!-- dummy object to open suggest popper -->
|
||||
<template #reference>
|
||||
<span></span>
|
||||
</template>
|
||||
</el-popover>
|
||||
<div>
|
||||
<el-popover placement="bottom" width="281" trigger="click" popper-class="new-toot-emoji-picker">
|
||||
<picker
|
||||
:data="emojiIndex"
|
||||
set="twitter"
|
||||
:autoFocus="true"
|
||||
@select="selectEmoji"
|
||||
:perLine="7"
|
||||
:emojiSize="24"
|
||||
:showPreview="false"
|
||||
:emojiTooltip="true"
|
||||
/>
|
||||
<template #reference>
|
||||
<el-button class="emoji-selector" link>
|
||||
<font-awesome-icon :icon="['far', 'face-smile']" size="lg" />
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import 'emoji-mart-vue-fast/css/emoji-mart.css'
|
||||
import data from 'emoji-mart-vue-fast/data/all.json'
|
||||
import { defineComponent, computed, toRefs, ref, onBeforeUnmount, onMounted, nextTick } from 'vue'
|
||||
import { Picker, EmojiIndex } from 'emoji-mart-vue-fast/src'
|
||||
import { useMagicKeys, whenever } from '@vueuse/core'
|
||||
|
||||
import suggestText from '@/utils/suggestText'
|
||||
import { useStore } from '@/store'
|
||||
import { ACTION_TYPES } from '@/store/TimelineSpace/Modals/NewToot/Status'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'status',
|
||||
components: {
|
||||
Picker
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
opened: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fixCursorPos: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 120
|
||||
}
|
||||
},
|
||||
emits: ['toot'],
|
||||
setup(props, ctx) {
|
||||
const space = 'TimelineSpace/Modals/NewToot/Status'
|
||||
const store = useStore()
|
||||
const { up, down, enter, escape, Ctrl_Enter, Cmd_Enter } = useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired(e) {
|
||||
if (e.key === 'Enter' && suggestOpened.value) e.preventDefault()
|
||||
if (e.key === 'ArrowUp' && suggestOpened.value) e.preventDefault()
|
||||
if (e.key === 'ArrowDown' && suggestOpened.value) e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
const { modelValue, fixCursorPos } = toRefs(props)
|
||||
const highlightedIndex = ref(0)
|
||||
const statusRef = ref<HTMLTextAreaElement>()
|
||||
const suggestRef = ref()
|
||||
const suggestOpened = ref<boolean>(false)
|
||||
|
||||
const filteredAccounts = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredAccounts)
|
||||
const filteredHashtags = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredHashtags)
|
||||
const filteredSuggestion = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.filteredSuggestion)
|
||||
const startIndex = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.startIndex)
|
||||
const matchWord = computed(() => store.state.TimelineSpace.Modals.NewToot.Status.matchWord)
|
||||
const customEmojis = computed(() => store.getters[`${space}/pickerEmojis`])
|
||||
const emojiIndex = new EmojiIndex(data, {
|
||||
custom: customEmojis.value
|
||||
})
|
||||
|
||||
whenever(up, () => {
|
||||
if (suggestOpened.value) suggestHighlight(highlightedIndex.value - 1)
|
||||
})
|
||||
whenever(down, () => {
|
||||
if (suggestOpened.value) suggestHighlight(highlightedIndex.value + 1)
|
||||
})
|
||||
whenever(enter, () => {
|
||||
if (suggestOpened.value) selectCurrentItem()
|
||||
})
|
||||
whenever(escape, () => {
|
||||
closeSuggest()
|
||||
})
|
||||
whenever(Ctrl_Enter, () => {
|
||||
ctx.emit('toot')
|
||||
})
|
||||
whenever(Cmd_Enter, () => {
|
||||
ctx.emit('toot')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
closeSuggest()
|
||||
})
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
statusRef.value?.focus()
|
||||
if (fixCursorPos.value) {
|
||||
statusRef.value?.setSelectionRange(0, 0)
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
})
|
||||
|
||||
const openSuggest = () => {
|
||||
suggestOpened.value = true
|
||||
}
|
||||
const closeSuggest = () => {
|
||||
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_SUGGEST}`)
|
||||
highlightedIndex.value = 0
|
||||
suggestOpened.value = false
|
||||
}
|
||||
const suggestAccount = async (start: number, word: string) => {
|
||||
try {
|
||||
await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_ACCOUNT}`, { word: word, start: start })
|
||||
openSuggest()
|
||||
return true
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
const suggestHashtag = async (start: number, word: string) => {
|
||||
try {
|
||||
await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_HASHTAG}`, { word: word, start: start })
|
||||
openSuggest()
|
||||
return true
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
const suggestEmoji = async (start: number, word: string) => {
|
||||
try {
|
||||
store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_EMOJI}`, { word: word, start: start })
|
||||
openSuggest()
|
||||
return true
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
const suggest = async (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
// e.target.sectionStart: Cursor position
|
||||
// e.target.value: current value of the textarea
|
||||
const [start, word] = suggestText(target.value, target.selectionStart!)
|
||||
if (!start || !word) {
|
||||
closeSuggest()
|
||||
return false
|
||||
}
|
||||
switch (word.charAt(0)) {
|
||||
case ':':
|
||||
await suggestEmoji(start, word)
|
||||
return true
|
||||
case '@':
|
||||
await suggestAccount(start, word)
|
||||
return true
|
||||
case '#':
|
||||
await suggestHashtag(start, word)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
const startSuggest = (e: Event) => {
|
||||
const currentValue = (e.target as HTMLInputElement).value
|
||||
// Start suggest after user stop writing
|
||||
setTimeout(async () => {
|
||||
if (currentValue === modelValue.value) {
|
||||
await suggest(e)
|
||||
}
|
||||
}, 700)
|
||||
}
|
||||
|
||||
const suggestHighlight = (index: number) => {
|
||||
if (index < 0) {
|
||||
highlightedIndex.value = 0
|
||||
} else if (index >= filteredSuggestion.value.length) {
|
||||
highlightedIndex.value = filteredSuggestion.value.length - 1
|
||||
} else {
|
||||
highlightedIndex.value = index
|
||||
}
|
||||
}
|
||||
const selectCurrentItem = () => {
|
||||
const item = filteredSuggestion.value[highlightedIndex.value]
|
||||
insertItem(item)
|
||||
}
|
||||
const insertItem = item => {
|
||||
if (!item) return
|
||||
if (item.code) {
|
||||
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.code} ${modelValue.value.slice(
|
||||
startIndex.value + matchWord.value.length
|
||||
)}`
|
||||
ctx.emit('update:modelValue', str)
|
||||
} else {
|
||||
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.name} ${modelValue.value.slice(
|
||||
startIndex.value + matchWord.value.length
|
||||
)}`
|
||||
ctx.emit('update:modelValue', str)
|
||||
}
|
||||
|
||||
closeSuggest()
|
||||
}
|
||||
const selectEmoji = emoji => {
|
||||
const current = statusRef.value?.selectionStart
|
||||
if (emoji.native) {
|
||||
ctx.emit('update:modelValue', `${modelValue.value.slice(0, current)}${emoji.native} ${modelValue.value.slice(current)}`)
|
||||
} else {
|
||||
// Custom emoji don't have native code
|
||||
ctx.emit('update:modelValue', `${modelValue.value.slice(0, current)}${emoji.name} ${modelValue.value.slice(current)}`)
|
||||
}
|
||||
closeSuggest()
|
||||
}
|
||||
const popperOptions = () => {
|
||||
const element = document.querySelector('#status_textarea')
|
||||
return {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: element,
|
||||
rootBoundary: 'viewport',
|
||||
altBoundary: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusRef,
|
||||
suggestRef,
|
||||
suggestOpened,
|
||||
emojiIndex,
|
||||
highlightedIndex,
|
||||
filteredAccounts,
|
||||
filteredHashtags,
|
||||
filteredSuggestion,
|
||||
startSuggest,
|
||||
suggestHighlight,
|
||||
insertItem,
|
||||
selectEmoji,
|
||||
popperOptions
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.suggest-popper {
|
||||
background-color: var(--theme-background-color);
|
||||
border: 1px solid var(--theme-header-menu-color);
|
||||
|
||||
.suggest-list {
|
||||
list-style: none;
|
||||
padding: 6px 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
li {
|
||||
font-size: var(--base-font-size);
|
||||
padding: 0 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
color: var(--theme-regular-color);
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background-color: var(--theme-selected-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.status {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: var(--base-font-size);
|
||||
background-color: var(--theme-background-color);
|
||||
|
||||
textarea {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 4px 32px 4px 16px;
|
||||
line-height: 1.5;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
font-size: inherit;
|
||||
color: var(--theme-primary-color);
|
||||
background-image: none;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
height: 120px;
|
||||
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 9.355, 1);
|
||||
word-break: normal;
|
||||
background-color: var(--theme-background-color);
|
||||
|
||||
&::placeholder {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-selector {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.emoji-picker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,5 +1,3 @@
|
||||
export class NewTootModalOpen extends Error {}
|
||||
|
||||
export class NewTootBlockSubmit extends Error {}
|
||||
|
||||
export class NewTootTootLength extends Error {}
|
||||
|
@ -121,10 +121,7 @@ const actions: ActionTree<TimelineSpaceState, RootState> = {
|
||||
// -----------------------------------------------
|
||||
// Shortcuts
|
||||
// -----------------------------------------------
|
||||
[ACTION_TYPES.WATCH_SHORTCUT_EVENTS]: ({ commit, dispatch, rootGetters }) => {
|
||||
win.ipcRenderer.on('CmdOrCtrl+N', () => {
|
||||
dispatch('TimelineSpace/Modals/NewToot/openModal', {}, { root: true })
|
||||
})
|
||||
[ACTION_TYPES.WATCH_SHORTCUT_EVENTS]: ({ commit, rootGetters }) => {
|
||||
win.ipcRenderer.on('CmdOrCtrl+K', () => {
|
||||
commit('TimelineSpace/Modals/Jump/changeModal', true, { root: true })
|
||||
})
|
||||
|
@ -1,4 +1,3 @@
|
||||
import NewToot, { NewTootModuleState } from './Modals/NewToot'
|
||||
import ImageViewer, { ImageViewerState } from './Modals/ImageViewer'
|
||||
import Jump, { JumpState } from './Modals/Jump'
|
||||
import ListMembership, { ListMembershipState } from './Modals/ListMembership'
|
||||
@ -17,7 +16,6 @@ type ModalsModule = {
|
||||
ImageViewer: ImageViewerState
|
||||
ListMembership: ListMembershipState
|
||||
MuteConfirm: MuteConfirmState
|
||||
NewToot: NewTootModuleState
|
||||
Report: ReportState
|
||||
Shortcut: ShortcutState
|
||||
}
|
||||
@ -29,14 +27,13 @@ const state = (): ModalsState => ({})
|
||||
const getters: GetterTree<ModalsState, RootState> = {
|
||||
modalOpened: (_state, _getters, rootState) => {
|
||||
const imageViewer = rootState.TimelineSpace.Modals.ImageViewer.modalOpen
|
||||
const newToot = rootState.TimelineSpace.Modals.NewToot.modalOpen
|
||||
const jump = rootState.TimelineSpace.Modals.Jump.modalOpen
|
||||
const listMembership = rootState.TimelineSpace.Modals.ListMembership.modalOpen
|
||||
const addListMember = rootState.TimelineSpace.Modals.AddListMember.modalOpen
|
||||
const shortcut = rootState.TimelineSpace.Modals.Shortcut.modalOpen
|
||||
const muteConfirm = rootState.TimelineSpace.Modals.MuteConfirm.modalOpen
|
||||
const report = rootState.TimelineSpace.Modals.Report.modalOpen
|
||||
return imageViewer || newToot || jump || listMembership || addListMember || shortcut || muteConfirm || report
|
||||
return imageViewer || jump || listMembership || addListMember || shortcut || muteConfirm || report
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,7 +41,6 @@ const Modals: Module<ModalsState, RootState> = {
|
||||
namespaced: true,
|
||||
modules: {
|
||||
ImageViewer,
|
||||
NewToot,
|
||||
Jump,
|
||||
ListMembership,
|
||||
AddListMember,
|
||||
|
@ -1,429 +0,0 @@
|
||||
import generator, { Entity } from 'megalodon'
|
||||
import Visibility, { VisibilityType } from '~/src/constants/visibility'
|
||||
import TootStatus, { StatusState } from './NewToot/Status'
|
||||
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
|
||||
import { RootState } from '@/store'
|
||||
import AxiosLoading from '@/utils/axiosLoading'
|
||||
import {
|
||||
NewTootModalOpen,
|
||||
NewTootBlockSubmit,
|
||||
NewTootTootLength,
|
||||
NewTootAttachLength,
|
||||
NewTootMediaDescription,
|
||||
NewTootPollInvalid,
|
||||
NewTootUnknownType
|
||||
} from '@/errors/validations'
|
||||
import { MyWindow } from '~/src/types/global'
|
||||
|
||||
const win = (window as any) as MyWindow
|
||||
|
||||
type MediaDescription = {
|
||||
id: string
|
||||
description: string
|
||||
}
|
||||
|
||||
type TootForm = {
|
||||
status: string
|
||||
spoiler: string
|
||||
polls: Array<string>
|
||||
pollExpireSeconds: number
|
||||
}
|
||||
|
||||
export type NewTootState = {
|
||||
modalOpen: boolean
|
||||
initialStatus: string
|
||||
initialSpoiler: string
|
||||
replyToMessage: Entity.Status | null
|
||||
quoteToMessage: Entity.Status | null
|
||||
blockSubmit: boolean
|
||||
attachedMedias: Array<Entity.Attachment>
|
||||
mediaDescriptions: { [key: string]: string | null }
|
||||
visibility: number
|
||||
sensitive: boolean
|
||||
attachedMediaCount: number
|
||||
pinedHashtag: boolean
|
||||
hashtags: Array<Entity.Tag>
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
type NewTootModule = {
|
||||
Status: StatusState
|
||||
}
|
||||
|
||||
export type NewTootModuleState = NewTootModule & NewTootState
|
||||
|
||||
const state = (): NewTootState => ({
|
||||
modalOpen: false,
|
||||
initialStatus: '',
|
||||
initialSpoiler: '',
|
||||
replyToMessage: null,
|
||||
quoteToMessage: null,
|
||||
blockSubmit: false,
|
||||
attachedMedias: [],
|
||||
mediaDescriptions: {},
|
||||
visibility: Visibility.Public.value,
|
||||
sensitive: false,
|
||||
attachedMediaCount: 0,
|
||||
pinedHashtag: false,
|
||||
hashtags: [],
|
||||
loading: false
|
||||
})
|
||||
|
||||
export const MUTATION_TYPES = {
|
||||
CHANGE_MODAL: 'changeModal',
|
||||
SET_REPLY_TO: 'setReplyTo',
|
||||
SET_QUOTE_TO: 'setQuoteTo',
|
||||
UPDATE_INITIAL_STATUS: 'updateInitialStatus',
|
||||
UPDATE_INITIAL_SPOILER: 'updateInitialSpoiler',
|
||||
CHANGE_BLOCK_SUBMIT: 'changeBlockSubmit',
|
||||
APPEND_ATTACHED_MEDIAS: 'appendAttachedMedias',
|
||||
CLEAR_ATTACHED_MEDIAS: 'clearAttachedMedias',
|
||||
REMOVE_MEDIA: 'removeMedia',
|
||||
UPDATE_MEDIA_DESCRIPTION: 'updateMediaDescription',
|
||||
CLEAR_MEDIA_DESCRIPTIONS: 'clearMediaDescriptions',
|
||||
REMOVE_MEDIA_DESCRIPTION: 'removeMediaDescription',
|
||||
CHANGE_VISIBILITY_VALUE: 'changeVisibilityValue',
|
||||
CHANGE_SENSITIVE: 'changeSensitive',
|
||||
UPDATE_MEDIA_COUNT: 'updateMediaCount',
|
||||
CHANGE_PINED_HASHTAG: 'changePinedHashtag',
|
||||
UPDATE_HASHTAGS: 'updateHashtags',
|
||||
CHANGE_LOADING: 'changeLoading'
|
||||
}
|
||||
|
||||
const mutations: MutationTree<NewTootState> = {
|
||||
[MUTATION_TYPES.CHANGE_MODAL]: (state, value: boolean) => {
|
||||
state.modalOpen = value
|
||||
},
|
||||
[MUTATION_TYPES.SET_REPLY_TO]: (state, message: Entity.Status | null) => {
|
||||
state.replyToMessage = message
|
||||
},
|
||||
[MUTATION_TYPES.SET_QUOTE_TO]: (state, message: Entity.Status | null) => {
|
||||
state.quoteToMessage = message
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_INITIAL_STATUS]: (state, status: string) => {
|
||||
state.initialStatus = status
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_INITIAL_SPOILER]: (state, cw: string) => {
|
||||
state.initialSpoiler = cw
|
||||
},
|
||||
[MUTATION_TYPES.CHANGE_BLOCK_SUBMIT]: (state, value: boolean) => {
|
||||
state.blockSubmit = value
|
||||
},
|
||||
[MUTATION_TYPES.APPEND_ATTACHED_MEDIAS]: (state, media: Entity.Attachment) => {
|
||||
state.attachedMedias = state.attachedMedias.concat([media])
|
||||
},
|
||||
[MUTATION_TYPES.CLEAR_ATTACHED_MEDIAS]: state => {
|
||||
state.attachedMedias = []
|
||||
},
|
||||
[MUTATION_TYPES.REMOVE_MEDIA]: (state, media: Entity.Attachment) => {
|
||||
state.attachedMedias = state.attachedMedias.filter(m => m.id !== media.id)
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_MEDIA_DESCRIPTION]: (state, value: MediaDescription) => {
|
||||
state.mediaDescriptions[value.id] = value.description
|
||||
},
|
||||
[MUTATION_TYPES.CLEAR_MEDIA_DESCRIPTIONS]: state => {
|
||||
state.mediaDescriptions = {}
|
||||
},
|
||||
[MUTATION_TYPES.REMOVE_MEDIA_DESCRIPTION]: (state, id: string) => {
|
||||
const descriptions = state.mediaDescriptions
|
||||
delete descriptions[id]
|
||||
state.mediaDescriptions = descriptions
|
||||
},
|
||||
/**
|
||||
* changeVisibilityValue
|
||||
* Update visibility using direct value
|
||||
* @param state vuex state object
|
||||
* @param value visibility value
|
||||
*/
|
||||
[MUTATION_TYPES.CHANGE_VISIBILITY_VALUE]: (state, value: number) => {
|
||||
state.visibility = value
|
||||
},
|
||||
[MUTATION_TYPES.CHANGE_SENSITIVE]: (state, value: boolean) => {
|
||||
state.sensitive = value
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_MEDIA_COUNT]: (state, count: number) => {
|
||||
state.attachedMediaCount = count
|
||||
},
|
||||
[MUTATION_TYPES.CHANGE_PINED_HASHTAG]: (state, value: boolean) => {
|
||||
state.pinedHashtag = value
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_HASHTAGS]: (state, tags: Array<Entity.Tag>) => {
|
||||
state.hashtags = tags
|
||||
},
|
||||
[MUTATION_TYPES.CHANGE_LOADING]: (state, value: boolean) => {
|
||||
state.loading = value
|
||||
}
|
||||
}
|
||||
|
||||
export const ACTION_TYPES = {
|
||||
SETUP_LOADING: 'setupLoading',
|
||||
TEARDOWN_LOADING: 'tearDownLoading',
|
||||
START_LOADING: 'startLoading',
|
||||
STOP_LOADING: 'stopLoading',
|
||||
UPDATE_MEDIA: 'updateMedia',
|
||||
POST_TOOT: 'postToot',
|
||||
OPEN_REPLY: 'openReply',
|
||||
OPEN_QUOTE: 'openQuote',
|
||||
OPEN_MODAL: 'openModal',
|
||||
CLOSE_MODAL: 'closeModal',
|
||||
UPLOAD_IMAGE: 'uploadImage',
|
||||
INCREMENT_MEDIA_COUNT: 'incrementMediaCount',
|
||||
DECREMENT_MEDIA_COUNT: 'decrementMediaCount',
|
||||
RESET_MEDIA_COUNT: 'resetMediaCount',
|
||||
REMOVE_MEDIA: 'removeMedia',
|
||||
UPDATE_HASHTAGS: 'updateHashtags',
|
||||
FETCH_VISIBILITY: 'fetchVisibility'
|
||||
}
|
||||
|
||||
const axiosLoading = new AxiosLoading()
|
||||
|
||||
const actions: ActionTree<NewTootState, RootState> = {
|
||||
[ACTION_TYPES.SETUP_LOADING]: ({ dispatch }) => {
|
||||
axiosLoading.on('start', (_: number) => {
|
||||
dispatch('startLoading')
|
||||
})
|
||||
axiosLoading.on('done', () => {
|
||||
dispatch('stopLoading')
|
||||
})
|
||||
},
|
||||
[ACTION_TYPES.TEARDOWN_LOADING]: () => {
|
||||
axiosLoading.removeAllListeners()
|
||||
},
|
||||
[ACTION_TYPES.START_LOADING]: ({ commit, state }) => {
|
||||
if (state.modalOpen && !state.loading) {
|
||||
commit(MUTATION_TYPES.CHANGE_LOADING, true)
|
||||
}
|
||||
},
|
||||
[ACTION_TYPES.STOP_LOADING]: ({ commit, state }) => {
|
||||
if (state.modalOpen && state.loading) {
|
||||
commit(MUTATION_TYPES.CHANGE_LOADING, false)
|
||||
}
|
||||
},
|
||||
[ACTION_TYPES.UPDATE_MEDIA]: async ({ rootState }, mediaDescription: MediaDescription) => {
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.server!.sns,
|
||||
rootState.TimelineSpace.server!.baseURL,
|
||||
rootState.TimelineSpace.account!.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
const attachments = Object.keys(mediaDescription).map(async id => {
|
||||
if (mediaDescription[id] !== null) {
|
||||
return client.updateMedia(id, { description: mediaDescription[id] })
|
||||
} else {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
})
|
||||
return Promise.all(attachments).catch(err => {
|
||||
console.error(err)
|
||||
throw err
|
||||
})
|
||||
},
|
||||
[ACTION_TYPES.POST_TOOT]: async ({ state, commit, rootState, dispatch }, params: TootForm): Promise<Entity.Status> => {
|
||||
if (!state.modalOpen) {
|
||||
throw new NewTootModalOpen()
|
||||
}
|
||||
|
||||
if (params.status.length < 1 || params.status.length > rootState.TimelineSpace.tootMax) {
|
||||
throw new NewTootTootLength()
|
||||
}
|
||||
|
||||
const visibilityKey: string | undefined = Object.keys(Visibility).find(key => {
|
||||
return Visibility[key].value === state.visibility
|
||||
})
|
||||
let specifiedVisibility: 'public' | 'unlisted' | 'private' | 'direct' = Visibility.Public.key
|
||||
if (visibilityKey !== undefined) {
|
||||
specifiedVisibility = Visibility[visibilityKey].key
|
||||
}
|
||||
|
||||
let form = {
|
||||
visibility: specifiedVisibility,
|
||||
sensitive: state.sensitive,
|
||||
spoiler_text: params.spoiler
|
||||
}
|
||||
|
||||
if (state.replyToMessage !== null) {
|
||||
form = Object.assign(form, {
|
||||
in_reply_to_id: state.replyToMessage.id
|
||||
})
|
||||
}
|
||||
|
||||
if (state.quoteToMessage !== null) {
|
||||
form = Object.assign(form, {
|
||||
quote_id: state.quoteToMessage.id
|
||||
})
|
||||
}
|
||||
|
||||
if (params.polls.length > 1) {
|
||||
params.polls.forEach(poll => {
|
||||
if (poll.length < 1) {
|
||||
throw new NewTootPollInvalid()
|
||||
}
|
||||
})
|
||||
form = Object.assign(form, {
|
||||
poll: {
|
||||
expires_in: params.pollExpireSeconds,
|
||||
multiple: false,
|
||||
options: params.polls
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (state.blockSubmit) {
|
||||
throw new NewTootBlockSubmit()
|
||||
}
|
||||
|
||||
if (state.attachedMedias.length > 0) {
|
||||
if (state.attachedMedias.length > 4) {
|
||||
throw new NewTootAttachLength()
|
||||
}
|
||||
form = Object.assign(form, {
|
||||
media_ids: state.attachedMedias.map(m => {
|
||||
return m.id
|
||||
})
|
||||
})
|
||||
// Update media descriptions
|
||||
await dispatch('updateMedia', state.mediaDescriptions).catch(_ => {
|
||||
throw new NewTootMediaDescription()
|
||||
})
|
||||
}
|
||||
|
||||
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, true)
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.server!.sns,
|
||||
rootState.TimelineSpace.server!.baseURL,
|
||||
rootState.TimelineSpace.account!.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
return client
|
||||
.postStatus(params.status, form)
|
||||
.then(res => {
|
||||
win.ipcRenderer.send('toot-action-sound')
|
||||
return res.data
|
||||
})
|
||||
.finally(() => {
|
||||
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, false)
|
||||
})
|
||||
},
|
||||
[ACTION_TYPES.OPEN_REPLY]: ({ commit, rootState }, message: Entity.Status) => {
|
||||
commit(MUTATION_TYPES.SET_REPLY_TO, message)
|
||||
const mentionAccounts = [message.account.acct]
|
||||
.concat(message.mentions.map(a => a.acct))
|
||||
.filter((a, i, self) => self.indexOf(a) === i)
|
||||
.filter(a => a !== rootState.TimelineSpace.account!.username)
|
||||
commit(MUTATION_TYPES.UPDATE_INITIAL_STATUS, `${mentionAccounts.map(m => `@${m}`).join(' ')} `)
|
||||
commit(MUTATION_TYPES.UPDATE_INITIAL_SPOILER, message.spoiler_text)
|
||||
commit(MUTATION_TYPES.CHANGE_MODAL, true)
|
||||
let value: number = Visibility.Public.value
|
||||
Object.keys(Visibility).forEach(key => {
|
||||
const target = Visibility[key]
|
||||
if (target.key === message.visibility) {
|
||||
value = target.value
|
||||
}
|
||||
})
|
||||
commit(MUTATION_TYPES.CHANGE_VISIBILITY_VALUE, value)
|
||||
},
|
||||
[ACTION_TYPES.OPEN_QUOTE]: ({ commit }, message: Entity.Status) => {
|
||||
commit(MUTATION_TYPES.SET_QUOTE_TO, message)
|
||||
commit(MUTATION_TYPES.CHANGE_MODAL, true)
|
||||
},
|
||||
[ACTION_TYPES.OPEN_MODAL]: ({ dispatch, commit, state }) => {
|
||||
if (!state.replyToMessage && state.pinedHashtag) {
|
||||
commit(MUTATION_TYPES.UPDATE_INITIAL_STATUS, state.hashtags.map(t => `#${t.name}`).join(' '))
|
||||
}
|
||||
commit(MUTATION_TYPES.CHANGE_MODAL, true)
|
||||
dispatch('fetchVisibility')
|
||||
},
|
||||
[ACTION_TYPES.CLOSE_MODAL]: ({ commit }) => {
|
||||
commit(MUTATION_TYPES.CHANGE_MODAL, false)
|
||||
commit(MUTATION_TYPES.UPDATE_INITIAL_STATUS, '')
|
||||
commit(MUTATION_TYPES.UPDATE_INITIAL_SPOILER, '')
|
||||
commit(MUTATION_TYPES.SET_REPLY_TO, null)
|
||||
commit(MUTATION_TYPES.SET_QUOTE_TO, null)
|
||||
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, false)
|
||||
commit(MUTATION_TYPES.CLEAR_ATTACHED_MEDIAS)
|
||||
commit(MUTATION_TYPES.CLEAR_MEDIA_DESCRIPTIONS)
|
||||
commit(MUTATION_TYPES.CHANGE_SENSITIVE, false)
|
||||
commit(MUTATION_TYPES.CHANGE_VISIBILITY_VALUE, Visibility.Public.value)
|
||||
},
|
||||
[ACTION_TYPES.UPLOAD_IMAGE]: async ({ commit, state, dispatch, rootState }, image: any) => {
|
||||
if (state.attachedMedias.length > 3) {
|
||||
throw new NewTootAttachLength()
|
||||
}
|
||||
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, true)
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.server!.sns,
|
||||
rootState.TimelineSpace.server!.baseURL,
|
||||
rootState.TimelineSpace.account!.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
return client
|
||||
.uploadMedia(image)
|
||||
.then(res => {
|
||||
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, false)
|
||||
if (res.data.type === 'unknown') throw new NewTootUnknownType()
|
||||
commit(MUTATION_TYPES.APPEND_ATTACHED_MEDIAS, res.data)
|
||||
dispatch(ACTION_TYPES.INCREMENT_MEDIA_COUNT)
|
||||
return res.data
|
||||
})
|
||||
.catch(err => {
|
||||
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, false)
|
||||
console.error(err)
|
||||
throw err
|
||||
})
|
||||
},
|
||||
[ACTION_TYPES.INCREMENT_MEDIA_COUNT]: ({ commit, state }) => {
|
||||
commit(MUTATION_TYPES.UPDATE_MEDIA_COUNT, state.attachedMediaCount + 1)
|
||||
},
|
||||
[ACTION_TYPES.DECREMENT_MEDIA_COUNT]: ({ commit, state }) => {
|
||||
commit(MUTATION_TYPES.UPDATE_MEDIA_COUNT, state.attachedMediaCount - 1)
|
||||
},
|
||||
[ACTION_TYPES.RESET_MEDIA_COUNT]: ({ commit }) => {
|
||||
commit(MUTATION_TYPES.UPDATE_MEDIA_COUNT, 0)
|
||||
},
|
||||
[ACTION_TYPES.REMOVE_MEDIA]: ({ commit, dispatch }, media: Entity.Attachment) => {
|
||||
commit(MUTATION_TYPES.REMOVE_MEDIA, media)
|
||||
commit(MUTATION_TYPES.REMOVE_MEDIA_DESCRIPTION, media.id)
|
||||
dispatch(ACTION_TYPES.DECREMENT_MEDIA_COUNT)
|
||||
},
|
||||
[ACTION_TYPES.UPDATE_HASHTAGS]: ({ commit, state }, tags: Array<Entity.Tag>) => {
|
||||
if (state.pinedHashtag && tags.length > 0) {
|
||||
commit(MUTATION_TYPES.UPDATE_HASHTAGS, tags)
|
||||
}
|
||||
},
|
||||
[ACTION_TYPES.FETCH_VISIBILITY]: async ({ commit, rootState }) => {
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.server!.sns,
|
||||
rootState.TimelineSpace.server!.baseURL,
|
||||
rootState.TimelineSpace.account!.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
const res = await client.verifyAccountCredentials()
|
||||
const visibility: VisibilityType | undefined = (Object.values(Visibility) as Array<VisibilityType>).find(v => {
|
||||
return res.data.source !== undefined && v.key === res.data.source.privacy
|
||||
})
|
||||
if (visibility === undefined) {
|
||||
throw new Error('Visibility value is invalid')
|
||||
}
|
||||
commit(MUTATION_TYPES.CHANGE_VISIBILITY_VALUE, visibility.value)
|
||||
return res.data
|
||||
}
|
||||
}
|
||||
|
||||
const getters: GetterTree<NewTootState, RootState> = {
|
||||
hashtagInserting: state => {
|
||||
return !state.replyToMessage && state.pinedHashtag
|
||||
}
|
||||
}
|
||||
|
||||
const NewToot: Module<NewTootState, RootState> = {
|
||||
namespaced: true,
|
||||
modules: {
|
||||
Status: TootStatus
|
||||
},
|
||||
state: state,
|
||||
mutations: mutations,
|
||||
getters: getters,
|
||||
actions: actions
|
||||
}
|
||||
|
||||
export default NewToot
|
@ -1,339 +0,0 @@
|
||||
import { EmojiIndex } from 'emoji-mart-vue-fast'
|
||||
import emojidata from 'emoji-mart-vue-fast/data/all.json'
|
||||
import generator, { MegalodonInterface } from 'megalodon'
|
||||
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
|
||||
import { RootState } from '@/store/index'
|
||||
import { LocalTag } from '~/src/types/localTag'
|
||||
import { InsertAccountCache } from '~/src/types/insertAccountCache'
|
||||
import { CachedAccount } from '~/src/types/cachedAccount'
|
||||
import { MyWindow } from '~/src/types/global'
|
||||
|
||||
const win = (window as any) as MyWindow
|
||||
|
||||
const emojiIndex = new EmojiIndex(emojidata)
|
||||
|
||||
type EmojiMartEmoji = {
|
||||
id: string
|
||||
name: string
|
||||
colons: string
|
||||
text: string
|
||||
emoticons: Array<string>
|
||||
skin: any
|
||||
native: string
|
||||
}
|
||||
|
||||
type Suggest = {
|
||||
name: string
|
||||
image?: string | null
|
||||
code?: string | null
|
||||
}
|
||||
|
||||
type SuggestAccount = Suggest
|
||||
|
||||
type SuggestHashtag = Suggest
|
||||
|
||||
type SuggestEmoji = Suggest
|
||||
|
||||
export type StatusState = {
|
||||
filteredSuggestion: Array<Suggest>
|
||||
filteredAccounts: Array<SuggestAccount>
|
||||
filteredHashtags: Array<SuggestHashtag>
|
||||
filteredEmojis: Array<SuggestEmoji>
|
||||
startIndex: number
|
||||
matchWord: string
|
||||
client: MegalodonInterface | null
|
||||
}
|
||||
|
||||
const state = (): StatusState => ({
|
||||
filteredSuggestion: [],
|
||||
filteredAccounts: [],
|
||||
filteredHashtags: [],
|
||||
filteredEmojis: [],
|
||||
startIndex: 0,
|
||||
matchWord: '',
|
||||
client: null
|
||||
})
|
||||
|
||||
export const MUTATION_TYPES = {
|
||||
APPEND_FILTERED_ACCOUNTS: 'appendFilteredAccounts',
|
||||
CLEAR_FILTERED_ACCOUNTS: 'clearFilteredAccounts',
|
||||
APPEND_FILTERED_HASHTAGS: 'appendFilteredHashtags',
|
||||
CLEAR_FILTERED_HASHTAGS: 'clearFilteredHashtags',
|
||||
UPDATE_FILTERED_EMOJIS: 'updateFilteredEmojis',
|
||||
CLEAR_FILTERED_EMOJIS: 'clearFilteredEmojis',
|
||||
CHANGE_START_INDEX: 'changeStartIndex',
|
||||
CHANGE_MATCH_WORD: 'changeMatchWord',
|
||||
FILTERED_SUGGESTION_FROM_HASHTAGS: 'filteredSuggestionFromHashtags',
|
||||
FILTERED_SUGGESTION_FROM_ACCOUNTS: 'filteredSuggestionFromAccounts',
|
||||
FILTERED_SUGGESTION_FROM_EMOJIS: 'filteredSuggestionFromEmojis',
|
||||
CLEAR_FILTERED_SUGGESTION: 'clearFilteredSuggestion',
|
||||
SET_CLIENT: 'setClient',
|
||||
CLEAR_CLIENT: 'clearClient'
|
||||
}
|
||||
|
||||
const mutations: MutationTree<StatusState> = {
|
||||
[MUTATION_TYPES.APPEND_FILTERED_ACCOUNTS]: (state, accounts: Array<string>) => {
|
||||
const suggestion = accounts.map(a => ({
|
||||
name: `@${a}`,
|
||||
image: null
|
||||
}))
|
||||
const appended = state.filteredAccounts.concat(suggestion)
|
||||
const unique = appended.filter((v1, i1, a1) => {
|
||||
return (
|
||||
a1.findIndex(v2 => {
|
||||
return v1.name === v2.name
|
||||
}) === i1
|
||||
)
|
||||
})
|
||||
state.filteredAccounts = unique.sort((a, b) => {
|
||||
if (a.name < b.name) return -1
|
||||
if (a.name > b.name) return 1
|
||||
return 0
|
||||
})
|
||||
},
|
||||
[MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS]: state => {
|
||||
state.filteredAccounts = []
|
||||
},
|
||||
[MUTATION_TYPES.APPEND_FILTERED_HASHTAGS]: (state, tags: Array<string>) => {
|
||||
const suggestion = tags.map(t => ({
|
||||
name: `#${t}`,
|
||||
image: null
|
||||
}))
|
||||
const appended = state.filteredHashtags.concat(suggestion)
|
||||
const unique = appended.filter((v1, i1, a1) => {
|
||||
return (
|
||||
a1.findIndex(v2 => {
|
||||
return v1.name === v2.name
|
||||
}) === i1
|
||||
)
|
||||
})
|
||||
Array.from(new Set(appended))
|
||||
state.filteredHashtags = unique.sort((a, b) => {
|
||||
if (a.name < b.name) return -1
|
||||
if (a.name > b.name) return 1
|
||||
return 0
|
||||
})
|
||||
},
|
||||
[MUTATION_TYPES.CLEAR_FILTERED_HASHTAGS]: state => {
|
||||
state.filteredHashtags = []
|
||||
},
|
||||
[MUTATION_TYPES.UPDATE_FILTERED_EMOJIS]: (state, emojis: Array<SuggestEmoji>) => {
|
||||
state.filteredEmojis = emojis
|
||||
},
|
||||
[MUTATION_TYPES.CHANGE_START_INDEX]: (state, index: number) => {
|
||||
state.startIndex = index
|
||||
},
|
||||
[MUTATION_TYPES.CHANGE_MATCH_WORD]: (state, word: string) => {
|
||||
state.matchWord = word
|
||||
},
|
||||
[MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS]: state => {
|
||||
state.filteredSuggestion = state.filteredHashtags
|
||||
},
|
||||
[MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS]: state => {
|
||||
state.filteredSuggestion = state.filteredAccounts
|
||||
},
|
||||
[MUTATION_TYPES.FILTERED_SUGGESTION_FROM_EMOJIS]: state => {
|
||||
state.filteredSuggestion = state.filteredEmojis
|
||||
},
|
||||
[MUTATION_TYPES.CLEAR_FILTERED_SUGGESTION]: state => {
|
||||
state.filteredSuggestion = []
|
||||
},
|
||||
[MUTATION_TYPES.SET_CLIENT]: (state, client: MegalodonInterface) => {
|
||||
state.client = client
|
||||
},
|
||||
[MUTATION_TYPES.CLEAR_CLIENT]: state => {
|
||||
state.client = null
|
||||
}
|
||||
}
|
||||
|
||||
type WordStart = {
|
||||
word: string
|
||||
start: number
|
||||
}
|
||||
|
||||
export const ACTION_TYPES = {
|
||||
SUGGEST_ACCOUNT: 'suggestAccount',
|
||||
SUGGEST_HASHTAG: 'suggestHashtag',
|
||||
SUGGEST_EMOJI: 'suggestEmoji',
|
||||
CANCEL_REQUEST: 'cancelRequest',
|
||||
CLOSE_SUGGEST: 'closeSuggest'
|
||||
}
|
||||
|
||||
const actions: ActionTree<StatusState, RootState> = {
|
||||
[ACTION_TYPES.SUGGEST_ACCOUNT]: async ({ commit, rootState, dispatch }, wordStart: WordStart) => {
|
||||
dispatch('cancelRequest')
|
||||
commit(MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS)
|
||||
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS)
|
||||
const { word, start } = wordStart
|
||||
const searchCache = async () => {
|
||||
const target = word.replace('@', '')
|
||||
const accounts: Array<CachedAccount> = await win.ipcRenderer.invoke('get-cache-accounts', rootState.TimelineSpace.account!.id)
|
||||
const matched = accounts.map(account => account.acct).filter(acct => acct.includes(target))
|
||||
if (matched.length === 0) throw new Error('Empty')
|
||||
commit(MUTATION_TYPES.APPEND_FILTERED_ACCOUNTS, matched)
|
||||
commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
|
||||
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
|
||||
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS)
|
||||
return matched
|
||||
}
|
||||
const searchAPI = async () => {
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.server!.sns,
|
||||
rootState.TimelineSpace.server!.baseURL,
|
||||
rootState.TimelineSpace.account!.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
commit(MUTATION_TYPES.SET_CLIENT, client)
|
||||
const res = await client.searchAccount(word)
|
||||
if (res.data.length === 0) throw new Error('Empty')
|
||||
commit(
|
||||
MUTATION_TYPES.APPEND_FILTERED_ACCOUNTS,
|
||||
res.data.map(account => account.acct)
|
||||
)
|
||||
await win.ipcRenderer.invoke('insert-cache-accounts', {
|
||||
ownerID: rootState.TimelineSpace.account!.id,
|
||||
accts: res.data.map(a => a.acct)
|
||||
} as InsertAccountCache)
|
||||
commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
|
||||
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
|
||||
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS)
|
||||
return res.data
|
||||
}
|
||||
await Promise.all([searchCache(), searchAPI()])
|
||||
},
|
||||
[ACTION_TYPES.SUGGEST_HASHTAG]: async ({ commit, rootState, dispatch }, wordStart: WordStart) => {
|
||||
dispatch('cancelRequest')
|
||||
commit(MUTATION_TYPES.CLEAR_FILTERED_HASHTAGS)
|
||||
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS)
|
||||
const { word, start } = wordStart
|
||||
const searchCache = async () => {
|
||||
const target = word.replace('#', '')
|
||||
const tags: Array<LocalTag> = await win.ipcRenderer.invoke('get-cache-hashtags')
|
||||
const matched = tags.map(tag => tag.tagName).filter(tag => tag.includes(target))
|
||||
if (matched.length === 0) throw new Error('Empty')
|
||||
commit(MUTATION_TYPES.APPEND_FILTERED_HASHTAGS, matched)
|
||||
commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
|
||||
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
|
||||
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS)
|
||||
return matched
|
||||
}
|
||||
const searchAPI = async () => {
|
||||
const client = generator(
|
||||
rootState.TimelineSpace.server!.sns,
|
||||
rootState.TimelineSpace.server!.baseURL,
|
||||
rootState.TimelineSpace.account!.accessToken,
|
||||
rootState.App.userAgent
|
||||
)
|
||||
commit(MUTATION_TYPES.SET_CLIENT, client)
|
||||
const res = await client.search(word, 'hashtags')
|
||||
if (res.data.hashtags.length === 0) throw new Error('Empty')
|
||||
commit(
|
||||
MUTATION_TYPES.APPEND_FILTERED_HASHTAGS,
|
||||
res.data.hashtags.map(tag => tag.name)
|
||||
)
|
||||
await win.ipcRenderer.invoke(
|
||||
'insert-cache-hashtags',
|
||||
res.data.hashtags.map(tag => tag.name)
|
||||
)
|
||||
commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
|
||||
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
|
||||
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS)
|
||||
return res.data.hashtags
|
||||
}
|
||||
await Promise.all([searchCache(), searchAPI()])
|
||||
},
|
||||
[ACTION_TYPES.SUGGEST_EMOJI]: ({ commit, rootState }, wordStart: WordStart) => {
|
||||
const { word, start } = wordStart
|
||||
// Find native emojis
|
||||
const foundEmoji: EmojiMartEmoji = emojiIndex.findEmoji(word)
|
||||
if (foundEmoji) {
|
||||
return {
|
||||
name: foundEmoji.colons,
|
||||
code: foundEmoji.native
|
||||
}
|
||||
}
|
||||
let filteredNativeEmoji: Array<SuggestEmoji> = []
|
||||
|
||||
const regexp = word.match(/^:(.+)/)
|
||||
if (regexp && regexp.length > 1) {
|
||||
const emojiName = regexp[1]
|
||||
const filteredEmoji: Array<EmojiMartEmoji> = emojiIndex.search(emojiName)
|
||||
filteredNativeEmoji = filteredEmoji.map((emoji: EmojiMartEmoji) => {
|
||||
return {
|
||||
name: emoji.colons,
|
||||
code: emoji.native
|
||||
}
|
||||
})
|
||||
}
|
||||
// Find custom emojis
|
||||
const filteredCustomEmoji: Array<Suggest> = rootState.TimelineSpace.emojis
|
||||
.map(emoji => {
|
||||
return {
|
||||
name: `:${emoji.shortcode}:`,
|
||||
image: emoji.url
|
||||
}
|
||||
})
|
||||
.filter(emoji => emoji.name.includes(word))
|
||||
const filtered: Array<SuggestEmoji> = filteredNativeEmoji.concat(filteredCustomEmoji)
|
||||
if (filtered.length === 0) throw new Error('Empty')
|
||||
commit(
|
||||
MUTATION_TYPES.UPDATE_FILTERED_EMOJIS,
|
||||
filtered.filter((e, i, array) => {
|
||||
return array.findIndex(ar => e.name === ar.name) === i
|
||||
})
|
||||
)
|
||||
commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
|
||||
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
|
||||
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_EMOJIS)
|
||||
return filtered
|
||||
},
|
||||
[ACTION_TYPES.CANCEL_REQUEST]: ({ state }) => {
|
||||
if (state.client) {
|
||||
state.client.cancel()
|
||||
}
|
||||
},
|
||||
[ACTION_TYPES.CLOSE_SUGGEST]: ({ commit, dispatch }) => {
|
||||
dispatch('cancelRequest')
|
||||
commit(MUTATION_TYPES.CHANGE_START_INDEX, 0)
|
||||
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, '')
|
||||
commit(MUTATION_TYPES.CLEAR_FILTERED_SUGGESTION)
|
||||
commit(MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS)
|
||||
commit(MUTATION_TYPES.CLEAR_FILTERED_HASHTAGS)
|
||||
commit(MUTATION_TYPES.CLEAR_CLIENT)
|
||||
}
|
||||
}
|
||||
|
||||
const getters: GetterTree<StatusState, RootState> = {
|
||||
pickerEmojis: (_state, _getters, rootState) => {
|
||||
return rootState.TimelineSpace.emojis
|
||||
.map(emoji => {
|
||||
return {
|
||||
name: `:${emoji.shortcode}:`,
|
||||
image: emoji.url
|
||||
}
|
||||
})
|
||||
.filter((e, i, array) => {
|
||||
return array.findIndex(ar => e.name === ar.name) === i
|
||||
})
|
||||
.map(e => {
|
||||
return {
|
||||
name: e.name,
|
||||
short_names: [e.name],
|
||||
text: e.name,
|
||||
emoticons: [],
|
||||
keywords: [e.name],
|
||||
imageUrl: e.image
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const Status: Module<StatusState, RootState> = {
|
||||
namespaced: true,
|
||||
state: state,
|
||||
mutations: mutations,
|
||||
actions: actions,
|
||||
getters: getters
|
||||
}
|
||||
|
||||
export default Status
|
Loading…
x
Reference in New Issue
Block a user