1
0
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:
AkiraFukushima 2023-02-07 01:16:45 +09:00
parent ebbf2ec15e
commit 5847875586
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
15 changed files with 67 additions and 2284 deletions

View File

@ -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",

View File

@ -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: [

View File

@ -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

View File

@ -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
}
}
})

View File

@ -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,

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +1,3 @@
export class NewTootModalOpen extends Error {}
export class NewTootBlockSubmit extends Error {}
export class NewTootTootLength extends Error {}

View File

@ -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 })
})

View File

@ -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,

View File

@ -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

View File

@ -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