Merge pull request #3310 from h3poteto/feat/composition

Rewrite Modals with composition API
This commit is contained in:
AkiraFukushima 2022-04-30 16:17:27 +09:00 committed by GitHub
commit a0ba665116
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1130 additions and 1033 deletions

View File

@ -4,15 +4,21 @@ describe('emojify', () => {
const emoji = [
{
shortcode: 'python',
url: 'https://example.com/python'
static_url: 'https://example.com/python',
url: 'https://example.com/python',
visible_in_picker: true
},
{
shortcode: 'nodejs',
url: 'https://example.com/nodejs'
static_url: 'https://example.com/nodejs',
url: 'https://example.com/nodejs',
visible_in_picker: true
},
{
shortcode: 'slack',
url: 'https://example.com/slack'
static_url: 'https://example.com/slack',
url: 'https://example.com/slack',
visible_in_picker: true
}
]
describe('Does not contain shortcode', () => {

View File

@ -119,7 +119,6 @@ export default {
return false
}
this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal')
this.$store.dispatch('TimelineSpace/Modals/NewToot/incrementMediaCount')
this.$store
.dispatch('TimelineSpace/Modals/NewToot/uploadImage', file)
.then(() => {

View File

@ -1,17 +1,18 @@
<template>
<div>
<new-toot></new-toot>
<jump></jump>
<new-toot v-if="newTootModal"></new-toot>
<jump v-if="jumpModal"></jump>
<image-viewer></image-viewer>
<list-membership></list-membership>
<add-list-member></add-list-member>
<mute-confirm></mute-confirm>
<list-membership v-if="listMembershipModal"></list-membership>
<add-list-member v-if="addListMemberModal"></add-list-member>
<mute-confirm v-if="muteConfirmModal"></mute-confirm>
<shortcut></shortcut>
<report></report>
<report v-if="reportModal"></report>
</div>
</template>
<script>
import { mapState } from 'vuex'
import NewToot from './Modals/NewToot'
import Jump from './Modals/Jump'
import ImageViewer from './Modals/ImageViewer'
@ -31,7 +32,17 @@ export default {
AddListMember,
MuteConfirm,
Shortcut,
Report,
Report
},
computed: {
...mapState({
newTootModal: state => state.TimelineSpace.Modals.NewToot.modalOpen,
jumpModal: state => state.TimelineSpace.Modals.Jump.modalOpen,
reportModal: state => state.TimelineSpace.Modals.Report.modalOpen,
muteConfirmModal: state => state.TimelineSpace.Modals.MuteConfirm.modalOpen,
addListMemberModal: state => state.TimelineSpace.Modals.AddListMember.modalOpen,
listMembershipModal: state => state.TimelineSpace.Modals.ListMembership.modalOpen
})
}
}
</script>

View File

@ -33,58 +33,68 @@
</div>
</template>
<script>
import { mapState } from 'vuex'
<script lang="ts">
import { defineComponent, computed, ref } from 'vue'
import { Entity } from 'megalodon'
import { ElMessage } from 'element-plus'
import { useI18next } from 'vue3-i18next'
import { useStore } from '@/store'
import { ACTION_TYPES } from '@/store/TimelineSpace/Modals/AddListMember'
import { ACTION_TYPES as LIST_ACTION_TYPES } from '@/store/TimelineSpace/Contents/Lists/Edit'
export default {
export default defineComponent({
name: 'add-list-member',
data() {
return {
name: ''
}
},
computed: {
...mapState({
loadingBackground: state => state.App.theme.wrapper_mask_color,
accounts: state => state.TimelineSpace.Modals.AddListMember.accounts,
listId: state => state.TimelineSpace.Modals.AddListMember.targetListId
}),
addListMemberModal: {
get() {
return this.$store.state.TimelineSpace.Modals.AddListMember.modalOpen
},
set(value) {
this.$store.dispatch('TimelineSpace/Modals/AddListMember/changeModal', value)
}
}
},
methods: {
username(account) {
setup() {
const space = 'TimelineSpace/Modals/AddListMember'
const store = useStore()
const i18n = useI18next()
const name = ref<string>('')
const loadingBackground = computed(() => store.state.App.theme.wrapper_mask_color)
const accounts = computed(() => store.state.TimelineSpace.Modals.AddListMember.accounts)
const listId = computed(() => store.state.TimelineSpace.Modals.AddListMember.targetListId)
const addListMemberModal = computed({
get: () => store.state.TimelineSpace.Modals.AddListMember.modalOpen,
set: (value: boolean) => store.dispatch(`${space}/${ACTION_TYPES.CHANGE_MODAL}`, value)
})
const username = (account: Entity.Account): string => {
if (account.display_name !== '') {
return account.display_name
} else {
return account.username
}
},
search() {
this.$store.dispatch('TimelineSpace/Modals/AddListMember/search', this.name)
},
add(user) {
this.$store
.dispatch('TimelineSpace/Modals/AddListMember/add', user)
}
const search = () => {
store.dispatch(`${space}/${ACTION_TYPES.SEARCH}`, name.value)
}
const add = (account: Entity.Account) => {
store
.dispatch(`${space}/${ACTION_TYPES.ADD}`, account)
.then(() => {
this.addListMemberModal = false
this.$store.dispatch('TimelineSpace/Contents/Lists/Edit/fetchMembers', this.listId)
store.dispatch(`${space}/${ACTION_TYPES.CHANGE_MODAL}`, false)
store.dispatch(`TimelineSpace/Contents/Lists/Edit/${LIST_ACTION_TYPES.FETCH_MEMBERS}`, listId.value)
})
.catch(() => {
this.$message({
message: this.$t('message.add_user_error'),
ElMessage({
message: i18n.t('message.add_user_error'),
type: 'error'
})
})
}
return {
name,
loadingBackground,
accounts,
addListMemberModal,
username,
search,
add
}
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -11,7 +11,7 @@
<span class="button-area"
><el-button type="text" v-show="showLeft" @click="decrementIndex()"> <font-awesome-icon icon="angle-left" /> </el-button
></span>
<Media :src="imageURL" :type="imageType"></Media>
<Media :src="imageURL" :imageType="imageType"></Media>
<span class="button-area"
><el-button type="text" v-show="showRight" @click="incrementIndex()"> <font-awesome-icon icon="angle-right" /> </el-button
></span>
@ -21,51 +21,49 @@
</transition>
</template>
<script>
import Media from './Media'
import { mapState } from 'vuex'
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'
import { ACTION_TYPES } from '@/store/TimelineSpace/Modals/ImageViewer'
import Media from './ImageViewer/Media.vue'
export default {
export default defineComponent({
name: 'image-viewer',
components: {
Media
},
name: 'image-viewer',
computed: {
...mapState({
modalOpen: state => state.TimelineSpace.Modals.ImageViewer.modalOpen
}),
imageURL() {
return this.$store.getters['TimelineSpace/Modals/ImageViewer/imageURL']
},
imageType() {
return this.$store.getters['TimelineSpace/Modals/ImageViewer/imageType']
},
showLeft() {
return this.$store.getters['TimelineSpace/Modals/ImageViewer/showLeft']
},
showRight() {
return this.$store.getters['TimelineSpace/Modals/ImageViewer/showRight']
setup() {
const space = 'TimelineSpace/Modals/ImageViewer'
const store = useStore()
const modalOpen = computed(() => store.state.TimelineSpace.Modals.ImageViewer.modalOpen)
const imageURL = computed(() => store.getters[`${space}/imageURL`])
const imageType = computed(() => store.getters[`${space}/imageType`])
const showLeft = computed(() => store.getters[`${space}/showLeft`])
const showRight = computed(() => store.getters[`${space}/showRight`])
const close = () => {
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_MODAL}`)
}
},
methods: {
close() {
this.$store.dispatch('TimelineSpace/Modals/ImageViewer/closeModal')
},
decrementIndex() {
if (this.showLeft) this.$store.dispatch('TimelineSpace/Modals/ImageViewer/decrementIndex')
},
incrementIndex() {
if (this.showRight) this.$store.dispatch('TimelineSpace/Modals/ImageViewer/incrementIndex')
},
closeHandle(event) {
switch (event.srcKey) {
case 'close':
this.close()
break
}
const decrementIndex = () => {
if (showLeft.value) store.dispatch(`${space}/${ACTION_TYPES.DECREMENT_INDEX}`)
}
const incrementIndex = () => {
if (showRight.value) store.dispatch(`${space}/${ACTION_TYPES.INCREMENT_INDEX}`)
}
return {
modalOpen,
imageURL,
imageType,
showLeft,
showRight,
close,
decrementIndex,
incrementIndex
}
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,80 @@
<template>
<div id="current-media" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.8)">
<video :src="imageSrc" v-if="isMovie()" autoplay loop controls v-on:loadstart="loaded()"></video>
<video :src="imageSrc" v-else-if="isGIF()" autoplay loop v-on:loadstart="loaded()"></video>
<video :src="imageSrc" v-else-if="isAudio()" autoplay loop controls v-on:loadstart="loaded()"></video>
<img :src="imageSrc" v-else v-on:load="loaded()" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref, toRefs, watch, computed } from 'vue'
import { useStore } from '@/store'
import { ACTION_TYPES } from '@/store/TimelineSpace/Modals/ImageViewer'
import exifImageUrl from '@/components/utils/exifImageUrl'
export default defineComponent({
name: 'Media',
props: {
src: {
type: String,
default: ''
},
imageType: {
type: String,
default: ''
}
},
setup(props) {
const srcRef = toRefs(props).src
const imageTypeRef = toRefs(props).imageType
const imageSrc = ref('')
imageSrc.value = srcRef.value
const store = useStore()
const loading = computed(() => store.state.TimelineSpace.Modals.ImageViewer.loading)
const isMovie = () => ['video'].includes(imageTypeRef.value)
const isGIF = () => ['gifv'].includes(imageTypeRef.value)
const isAudio = () => ['audio'].includes(imageTypeRef.value)
watch(srcRef, async (newSrc, _oldSrc) => {
imageSrc.value = newSrc
if (newSrc && !isMovie() && !isGIF() && !isAudio()) {
try {
const transformed = await exifImageUrl(newSrc)
imageSrc.value = transformed
} catch (err) {
console.error(err)
}
}
})
const loaded = () => store.dispatch(`TimelineSpace/Modals/ImageViewer/${ACTION_TYPES.LOADED}`)
return {
imageSrc,
loading,
isMovie,
isGIF,
isAudio,
loaded
}
}
})
</script>
<style lang="scss" scoped>
#current-media {
max-width: 80%;
min-width: 10%;
height: 80%;
display: inline-flex;
img,
video {
max-height: 100%;
max-width: 100%;
align-self: center;
}
}
</style>

View File

@ -1,11 +1,11 @@
<template>
<el-dialog v-model="jumpModal" width="440px" class="jump-modal">
<el-form class="jump-form" v-on:submit.prevent="jump">
<el-dialog v-model="jumpModal" width="440px" custom-class="jump-modal">
<el-form class="jump-form" v-on:submit.prevent="jumpCurrentSelected">
<div class="channel">
<input type="text" v-model="channel" :placeholder="$t('modals.jump.jump_to')" ref="channel" />
<input type="text" v-model="inputtedChannel" :placeholder="$t('modals.jump.jump_to')" ref="channelForm" v-focus autofocus />
<ul class="channel-list">
<li
v-for="c in filterdChannel"
v-for="c in filteredChannel"
:class="c.name === selectedChannel.name ? 'channel-list-item selected' : 'channel-list-item'"
@click="jump(c)"
@mouseover="changeSelected(c)"
@ -22,107 +22,69 @@
</el-dialog>
</template>
<script>
import { mapState } from 'vuex'
<script lang="ts">
import { defineComponent, computed, watch } from 'vue'
import { useStore } from '@/store'
import { MUTATION_TYPES, ACTION_TYPES, Channel } from '@/store/TimelineSpace/Modals/Jump'
export default {
export default defineComponent({
name: 'jump',
computed: {
...mapState('TimelineSpace/Modals/Jump', {
channelList: state => state.defaultChannelList.concat(state.tagChannelList).concat(state.listChannelList),
selectedChannel: state => state.selectedChannel
}),
channel: {
get() {
return this.$store.state.TimelineSpace.Modals.Jump.channel
},
set(value) {
this.$store.commit('TimelineSpace/Modals/Jump/updateChannel', value)
}
},
filterdChannel() {
return this.filterChannelForm()
},
jumpModal: {
get() {
return this.$store.state.TimelineSpace.Modals.Jump.modalOpen
},
set(value) {
this.$store.commit('TimelineSpace/Modals/Jump/changeModal', value)
}
},
shortcutEnabled: function () {
return this.jumpModal
}
},
watch: {
channel: function (_newChannel, _oldChannel) {
this.$store.commit('TimelineSpace/Modals/Jump/changeSelected', this.filterChannelForm()[0])
},
jumpModal: function (newModal, oldModal) {
if (!oldModal && newModal) {
this.$nextTick(function () {
this.$store.dispatch('TimelineSpace/Modals/Jump/syncListChannel')
this.$store.dispatch('TimelineSpace/Modals/Jump/syncTagChannel')
this.$refs.channel.focus()
})
setup() {
const space = 'TimelineSpace/Modals/Jump'
const store = useStore()
const channelList = computed(() =>
store.state.TimelineSpace.Modals.Jump.defaultChannelList
.concat(store.state.TimelineSpace.Modals.Jump.tagChannelList)
.concat(store.state.TimelineSpace.Modals.Jump.listChannelList)
)
const selectedChannel = computed(() => store.state.TimelineSpace.Modals.Jump.selectedChannel)
const inputtedChannel = computed({
get: () => store.state.TimelineSpace.Modals.Jump.channel,
set: (value: string) => store.commit(`${space}/${MUTATION_TYPES.UPDATE_CHANNEL}`, value)
})
const jumpModal = computed({
get: () => store.state.TimelineSpace.Modals.Jump.modalOpen,
set: (value: boolean) => store.commit(`${space}/${MUTATION_TYPES.CHANGE_MODAL}`, value)
})
// const shortcutEnabled = computed(() => jumpModal.value)
const filteredChannel = computed(() =>
channelList.value.filter(c => c.name.toLowerCase().indexOf(inputtedChannel.value.toLowerCase()) !== -1)
)
watch(inputtedChannel, (_new, _old) => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_SELECTED}`, filteredChannel.value[0])
})
watch(jumpModal, (newValue, oldValue) => {
if (!oldValue && newValue) {
store.dispatch(`${space}/${ACTION_TYPES.SYNC_LIST_CHANNEL}`)
store.dispatch(`${space}/${ACTION_TYPES.SYNC_TAG_CHANNEL}`)
} else {
this.channel = ''
store.commit(`${space}/${MUTATION_TYPES.UPDATE_CHANNEL}`, '')
}
})
const changeSelected = (channel: Channel) => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_SELECTED}`, channel)
}
},
methods: {
filterChannelForm() {
return this.channelList.filter(c => {
return c.name.toLowerCase().indexOf(this.channel.toLowerCase()) !== -1
})
},
nextChannel() {
const filterd = this.filterChannelForm()
const i = filterd.findIndex(e => {
return e.name === this.selectedChannel.name
})
if (i !== undefined && i < filterd.length - 1) {
this.$store.commit('TimelineSpace/Modals/Jump/changeSelected', filterd[i + 1])
}
},
prevChannel() {
const filterd = this.filterChannelForm()
const i = filterd.findIndex(e => {
return e.name === this.selectedChannel.name
})
if (i !== undefined && i > 0) {
this.$store.commit('TimelineSpace/Modals/Jump/changeSelected', filterd[i - 1])
}
},
changeSelected(channel) {
this.$store.commit('TimelineSpace/Modals/Jump/changeSelected', channel)
},
jumpCurrentSelected() {
if (this.jumpModal) {
this.$store.dispatch('TimelineSpace/Modals/Jump/jumpCurrentSelected')
}
},
jump(channel) {
this.$store.dispatch('TimelineSpace/Modals/Jump/jump', channel)
},
handleKey(event) {
switch (event.srcKey) {
case 'next':
this.nextChannel()
break
case 'prev':
this.prevChannel()
break
case 'select':
this.jumpCurrentSelected()
break
default:
return true
}
const jump = (channel: Channel) => {
store.dispatch(`${space}/${ACTION_TYPES.JUMP}`, channel)
}
const jumpCurrentSelected = () => {
store.dispatch(`${space}/${ACTION_TYPES.JUMP_CURRENT_SELECTED}`)
}
return {
selectedChannel,
inputtedChannel,
jumpModal,
filteredChannel,
jump,
changeSelected,
jumpCurrentSelected
}
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template>
<el-dialog :title="$t('modals.list_membership.title')" v-model="listMembershipModal" width="400px" class="list-membership-modal">
<el-dialog :title="$t('modals.list_membership.title')" v-model="listMembershipModal" width="400px" custom-class="list-membership-modal">
<el-checkbox-group v-model="belongToLists" v-loading="loading">
<table class="lists">
<tbody>
@ -14,71 +14,66 @@
</el-dialog>
</template>
<script>
import { mapState } from 'vuex'
<script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useI18next } from 'vue3-i18next'
import { useStore } from '@/store'
import { ACTION_TYPES } from '@/store/TimelineSpace/Modals/ListMembership'
export default {
export default defineComponent({
name: 'list-membership',
data() {
return {
loading: false
}
},
computed: {
...mapState({
account: state => state.TimelineSpace.Modals.ListMembership.account,
lists: state => state.TimelineSpace.Modals.ListMembership.lists
}),
listMembershipModal: {
get() {
return this.$store.state.TimelineSpace.Modals.ListMembership.modalOpen
},
set(value) {
this.$store.dispatch('TimelineSpace/Modals/ListMembership/changeModal', value)
}
},
belongToLists: {
get() {
return this.$store.state.TimelineSpace.Modals.ListMembership.belongToLists.map(l => l.id)
},
set(value) {
this.loading = true
return this.$store
.dispatch('TimelineSpace/Modals/ListMembership/changeBelongToLists', value)
setup() {
const space = 'TimelineSpace/Modals/ListMembership'
const loading = ref<boolean>(false)
const store = useStore()
const i18n = useI18next()
const account = computed(() => store.state.TimelineSpace.Modals.ListMembership.account)
const lists = computed(() => store.state.TimelineSpace.Modals.ListMembership.lists)
const listMembershipModal = computed({
get: () => store.state.TimelineSpace.Modals.ListMembership.modalOpen,
set: (value: boolean) => store.dispatch(`${space}/${ACTION_TYPES.CHANGE_MODAL}`, value)
})
const belongToLists = computed({
get: () => store.state.TimelineSpace.Modals.ListMembership.belongToLists.map(l => l.id),
set: (value: Array<string>) => {
loading.value = true
store
.dispatch(`${space}/${ACTION_TYPES.CHANGE_BELONG_TO_LISTS}`, value)
.catch(() => {
this.$message({
message: this.$t('message.update_list_memberships_error'),
ElMessage({
message: i18n.t('message.update_list_memberships_error'),
type: 'error'
})
})
.finally(() => (this.loading = false))
.finally(() => (loading.value = false))
}
}
},
watch: {
listMembershipModal: function (newState, oldState) {
if (!oldState && newState) {
this.init()
}
}
},
methods: {
async init() {
this.loading = true
})
onMounted(async () => {
loading.value = true
try {
await this.$store.dispatch('TimelineSpace/Modals/ListMembership/fetchListMembership', this.account)
await this.$store.dispatch('TimelineSpace/Modals/ListMembership/fetchLists')
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_LIST_MEMBERSHIP}`, account.value)
await store.dispatch(`${space}/${ACTION_TYPES.FETCH_LISTS}`)
} catch (err) {
this.$message({
message: this.$t('message.lists_fetch_error'),
ElMessage({
message: i18n.t('message.lists_fetch_error'),
type: 'error'
})
} finally {
this.loading = false
loading.value = false
}
})
return {
loading,
lists,
listMembershipModal,
belongToLists
}
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -1,103 +0,0 @@
<template>
<div
id="current-media"
v-loading="loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
>
<video
:src="src"
v-if="isMovieFile()"
autoplay
loop
controls
v-on:loadstart="loaded()"
></video>
<video
:src="src"
v-else-if="isGIF()"
autoplay
loop
v-on:loadstart="loaded()"
></video>
<video
:src="src"
v-else-if="isAudio()"
autoplay
loop
controls
v-on:loadstart="loaded()"
></video>
<img :src="imageSrc" v-else v-on:load="loaded()" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import exifImageUrl from '@/components/utils/exifImageUrl'
export default {
props: {
src: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
},
data() {
return {
imageSrc: this.src,
}
},
watch: {
src: async function (newSrc, _oldSrc) {
this.imageSrc = newSrc
if (newSrc && !this.isMovieFile() && !this.isGIF()) {
try {
const transformed = await exifImageUrl(newSrc)
this.imageSrc = transformed
} catch (err) {
console.error(err)
}
}
},
},
computed: {
...mapState({
loading: (state) => state.TimelineSpace.Modals.ImageViewer.loading,
}),
},
methods: {
isMovieFile() {
return ['video'].includes(this.type)
},
isGIF() {
return ['gifv'].includes(this.type)
},
isAudio() {
return ['audio'].includes(this.type)
},
async loaded() {
this.$store.dispatch('TimelineSpace/Modals/ImageViewer/loaded')
},
},
}
</script>
<style lang="scss" scoped>
#current-media {
max-width: 80%;
min-width: 10%;
height: 80%;
display: inline-flex;
img,
video {
max-height: 100%;
max-width: 100%;
align-self: center;
}
}
</style>

View File

@ -12,39 +12,40 @@
</el-dialog>
</template>
<script>
import { mapState } from 'vuex'
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
import { useStore } from '@/store'
import { ACTION_TYPES } from '@/store/TimelineSpace/Modals/MuteConfirm'
export default {
export default defineComponent({
name: 'MuteConfirm',
data() {
setup() {
const space = 'TimelineSpace/Modals/MuteConfirm'
const store = useStore()
const notify = ref<boolean>(true)
const muteConfirmModal = computed({
get: () => store.state.TimelineSpace.Modals.MuteConfirm.modalOpen,
set: (value: boolean) => store.dispatch(`${space}/${ACTION_TYPES.CHANGE_MODAL}`, value)
})
const closeModal = () => {
store.dispatch(`${space}/${ACTION_TYPES.CHANGE_MODAL}`, false)
}
const submit = async () => {
closeModal()
await store.dispatch(`${space}/${ACTION_TYPES.SUBMIT}`, notify.value)
}
return {
notify: true
}
},
computed: {
...mapState('TimelineSpace/Modals/MuteConfirm', {
account: state => state.account
}),
muteConfirmModal: {
get() {
return this.$store.state.TimelineSpace.Modals.MuteConfirm.modalOpen
},
set(value) {
this.$store.dispatch('TimelineSpace/Modals/MuteConfirm/changeModal', value)
}
}
},
methods: {
closeModal() {
this.muteConfirmModal = false
},
async submit() {
this.closeModal()
await this.$store.dispatch('TimelineSpace/Modals/MuteConfirm/submit', this.notify)
notify,
muteConfirmModal,
closeModal,
submit
}
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -7,36 +7,34 @@
:before-close="closeConfirm"
width="600px"
custom-class="new-toot-modal"
ref="dialog"
ref="dialogRef"
>
<el-form v-on:submit.prevent="toot" role="form">
<Quote :message="quoteToMessage" :displayNameStyle="displayNameStyle" v-if="quoteToMessage !== null" ref="quote"></Quote>
<div class="spoiler" v-if="showContentWarning" ref="spoiler">
<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="spoiler" />
<input type="text" class="el-input__inner" :placeholder="$t('modals.new_toot.cw')" v-model="spoilerText" />
</div>
</div>
<Status
v-model="status"
:modelValue="statusText"
@update:modelValue="statusText = $event"
:opened="newTootModal"
:fixCursorPos="hashtagInserting"
:height="statusHeight"
@paste="onPaste"
@toot="toot"
@pickerOpened="innerElementOpened"
@suggestOpened="innerElementOpened"
/>
</el-form>
<Poll
v-if="openPoll"
v-model="polls"
v-model:polls="polls"
v-model:expire="pollExpire"
@addPoll="addPoll"
@removePoll="removePoll"
:defaultExpire="pollExpire"
@changeExpire="changeExpire"
ref="poll"
ref="pollRef"
></Poll>
<div class="preview" ref="preview">
<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 type="text" @click="removeAttachment(media)" class="remove-image"><font-awesome-icon icon="circle-xmark" /></el-button>
@ -59,7 +57,7 @@
<el-button size="default" type="text" @click="selectImage" :title="$t('modals.new_toot.footer.add_image')">
<font-awesome-icon icon="camera" />
</el-button>
<input name="image" type="file" class="image-input" ref="image" @change="onChangeImage" :key="attachedMediaId" />
<input name="image" type="file" class="image-input" ref="imageRef" @change="onChangeImage" />
</div>
<div class="poll">
<el-button size="default" type="text" @click="togglePollForm" :title="$t('modals.new_toot.footer.poll')">
@ -131,7 +129,7 @@
</div>
<div class="info">
<img src="../../../assets/images/loading-spinner-wide.svg" v-show="loading" class="loading" />
<span class="text-count">{{ tootMax - status.length }}</span>
<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>
@ -144,335 +142,366 @@
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
<script lang="ts">
import { defineComponent, ref, reactive, computed, onMounted, ComponentPublicInstance, nextTick } from 'vue'
import { useI18next } from 'vue3-i18next'
import { ElMessage, ElMessageBox, ElDialog } from 'element-plus'
import { Entity } from 'megalodon'
import { useStore } from '@/store'
import Visibility from '~/src/constants/visibility'
import Status from './NewToot/Status'
import Poll from './NewToot/Poll'
import Quote from './NewToot/Quote'
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 '~/src/renderer/components/event'
import { EventEmitter } from '@/components/event'
import { ACTION_TYPES, MUTATION_TYPES } from '@/store/TimelineSpace/Modals/NewToot'
export default {
export default defineComponent({
name: 'new-toot',
components: {
Status,
Poll,
Quote
},
data() {
return {
status: '',
spoiler: '',
showContentWarning: false,
visibilityList: Visibility,
openPoll: false,
polls: [],
pollExpire: {
label: this.$t('modals.new_toot.poll.expires.1_day'),
value: 3600 * 24
},
statusHeight: 240
}
},
computed: {
...mapState('TimelineSpace/Modals/NewToot', {
replyToId: state => {
if (state.replyToMessage !== null) {
return state.replyToMessage.id
} else {
return null
}
},
quoteToMessage: state => state.quoteToMessage,
attachedMedias: state => state.attachedMedias,
attachedMediaId: state => state.attachedMediaId,
mediaDescriptions: state => state.mediaDescriptions,
blockSubmit: state => state.blockSubmit,
visibility: state => state.visibility,
sensitive: state => state.sensitive,
initialStatus: state => state.initialStatus,
initialSpoiler: state => state.initialSpoiler,
visibilityIcon: state => {
switch (state.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'
}
},
loading: state => state.loading
}),
...mapState('TimelineSpace', {
tootMax: state => state.tootMax
}),
...mapState('App', {
displayNameStyle: state => state.displayNameStyle
}),
...mapGetters('TimelineSpace/Modals/NewToot', ['hashtagInserting']),
newTootModal: {
get() {
return this.$store.state.TimelineSpace.Modals.NewToot.modalOpen
},
set(value) {
if (value) {
this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal')
} else {
this.$store.dispatch('TimelineSpace/Modals/NewToot/closeModal')
}
}
},
pinedHashtag: {
get() {
return this.$store.state.TimelineSpace.Modals.NewToot.pinedHashtag
},
set(value) {
this.$store.commit('TimelineSpace/Modals/NewToot/changePinedHashtag', value)
}
}
},
created() {
this.$store.dispatch('TimelineSpace/Modals/NewToot/setupLoading')
},
mounted() {
EventEmitter.on('image-uploaded', () => {
if (this.$refs.preview) {
this.statusHeight = this.statusHeight - this.$refs.preview.offsetHeight
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 dialogRef = ref<InstanceType<typeof ElDialog>>()
const quoteRef = ref<ComponentPublicInstance>()
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'
}
})
},
watch: {
newTootModal: function (newState, oldState) {
if (!oldState && newState) {
this.showContentWarning = this.initialSpoiler
this.status = this.initialStatus
this.spoiler = this.initialSpoiler
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)
})
store.dispatch(`${space}/${ACTION_TYPES.SETUP_LOADING}`)
onMounted(() => {
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
})
const close = () => {
store.dispatch(`${space}/${ACTION_TYPES.RESET_MEDIA_COUNT}`)
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_MODAL}`)
}
},
methods: {
close() {
this.filteredAccount = []
const spoilerHeight = this.$refs.spoiler ? this.$refs.spoiler.offsetHeight : 0
this.showContentWarning = false
this.spoiler = ''
this.statusHeight = this.statusHeight + spoilerHeight
const pollHeight = this.$refs.poll ? this.$refs.poll.$el.offsetHeight : 0
this.openPoll = false
this.polls = []
this.pollExpire = {
label: this.$t('modals.new_toot.poll.expires.1_day'),
value: 3600 * 24
}
this.statusHeight = this.statusHeight + pollHeight
const quoteHeight = this.$refs.quote ? this.$refs.quote.$el.offsetHeight : 0
this.statusHeight = this.statusHeight + quoteHeight
const attachmentHeight = this.$refs.preview ? this.$refs.preview.offsetHeight : 0
this.statusHeight = this.statusHeight + attachmentHeight
this.$store.dispatch('TimelineSpace/Modals/NewToot/resetMediaCount')
this.$store.dispatch('TimelineSpace/Modals/NewToot/closeModal')
},
async toot() {
const toot = async () => {
const form = {
status: this.status,
spoiler: this.spoiler,
polls: this.polls,
pollExpireSeconds: this.pollExpire.value
status: statusText.value,
spoiler: spoilerText.value,
polls: polls.value,
pollExpireSeconds: pollExpire.value
}
try {
const status = await this.$store.dispatch('TimelineSpace/Modals/NewToot/postToot', form)
this.$store.dispatch('TimelineSpace/Modals/NewToot/updateHashtags', status.tags)
this.close()
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) {
this.$message({
message: this.$t('validation.new_toot.toot_length', {
ElMessage({
message: i18n.t('validation.new_toot.toot_length', {
min: 1,
max: this.tootMax
max: tootMax.value
}),
type: 'error'
})
} else if (err instanceof NewTootAttachLength) {
this.$message({
message: this.$t('validation.new_toot.attach_length', { max: 4 }),
ElMessage({
message: i18n.t('validation.new_toot.attach_length', { max: 4 }),
type: 'error'
})
} else if (err instanceof NewTootPollInvalid) {
this.$message({
message: this.$t('validation.new_toot.poll_invalid'),
ElMessage({
message: i18n.t('validation.new_toot.poll_invalid'),
type: 'error'
})
} else if (err instanceof NewTootModalOpen || err instanceof NewTootBlockSubmit) {
// Nothing
} else {
this.$message({
message: this.$t('message.toot_error'),
ElMessage({
message: i18n.t('message.toot_error'),
type: 'error'
})
}
}
},
selectImage() {
this.$refs.image.click()
},
onChangeImage(e) {
if (e.target.files.item(0) === null || e.target.files.item(0) === undefined) {
}
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
}
const file = e.target.files.item(0)
if (!file.type.includes('image') && !file.type.includes('video')) {
this.$message({
message: this.$t('validation.new_toot.attach_image'),
ElMessage({
message: i18n.t('validation.new_toot.attach_image'),
type: 'error'
})
return
}
this.updateImage(file)
},
onPaste(e) {
const mimeTypes = window.clipboard.availableFormats().filter(type => type.startsWith('image'))
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.clipboard.readImage()
let data
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] })
this.updateImage(file)
},
updateImage(file) {
this.$store.dispatch('TimelineSpace/Modals/NewToot/incrementMediaCount')
this.$store
.dispatch('TimelineSpace/Modals/NewToot/uploadImage', file)
.then(() => {
this.statusHeight = this.statusHeight - this.$refs.preview.offsetHeight
})
.catch(err => {
if (err instanceof NewTootAttachLength) {
this.$message({
message: this.$t('validation.new_toot.attach_length', { max: 4 }),
type: 'error'
})
} else {
this.$message({
message: this.$t('message.attach_error'),
type: 'error'
})
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
})
},
removeAttachment(media) {
const previousHeight = this.$refs.preview.offsetHeight
this.$store.dispatch('TimelineSpace/Modals/NewToot/removeMedia', media).then(() => {
this.statusHeight = this.statusHeight + previousHeight
})
},
changeVisibility(level) {
this.$store.commit('TimelineSpace/Modals/NewToot/changeVisibilityValue', level)
},
changeSensitive() {
this.$store.commit('TimelineSpace/Modals/NewToot/changeSensitive', !this.sensitive)
},
closeConfirm(done) {
if (this.status.length === 0) {
}
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 (statusText.value.length === 0) {
done()
} else {
this.$confirm(this.$t('modals.new_toot.close_confirm'), {
confirmButtonText: this.$t('modals.new_toot.close_confirm_ok'),
cancelButtonText: this.$t('modals.new_toot.close_confirm_cancel')
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(_ => {})
}
},
updateDescription(id, value) {
this.$store.commit('TimelineSpace/Modals/NewToot/updateMediaDescription', { id: id, description: value })
},
async togglePollForm() {
const previousHeight = this.$refs.poll ? this.$refs.poll.$el.offsetHeight : 0
}
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 = () => {
this.openPoll = !this.openPoll
if (this.openPoll) {
this.polls = ['', '']
openPoll.value = !openPoll.value
if (openPoll.value) {
polls.value = ['', '']
} else {
this.polls = []
polls.value = []
}
}
await toggle()
if (this.openPoll) {
this.statusHeight = this.statusHeight - this.$refs.poll.$el.offsetHeight
} else {
this.statusHeight = this.statusHeight + previousHeight
}
},
async addPoll() {
const previousPollHeight = this.$refs.poll.$el.offsetHeight
await this.polls.push('')
const diff = this.$refs.poll.$el.offsetHeight - previousPollHeight
this.statusHeight = this.statusHeight - diff
},
async removePoll(id) {
const previousPollHeight = this.$refs.poll.$el.offsetHeight
await this.polls.splice(id, 1)
const diff = previousPollHeight - this.$refs.poll.$el.offsetHeight
this.statusHeight = this.statusHeight + diff
},
changeExpire(obj) {
this.pollExpire = obj
},
async toggleContentWarning() {
const previousHeight = this.$refs.spoiler ? this.$refs.spoiler.offsetHeight : 0
await (this.showContentWarning = !this.showContentWarning)
if (this.showContentWarning) {
this.statusHeight = this.statusHeight - this.$refs.spoiler.offsetHeight
} else {
this.statusHeight = this.statusHeight + previousHeight
}
},
handleResize(event) {
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 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 vHeight = this.$refs.dialog.$el.firstChild.style.marginTop
const marginTop = (document.documentElement.clientHeight / 100) * parseInt(vHeight)
const limitHeight = document.documentElement.clientHeight - marginTop - 80
if (this.$refs.dialog.$el.firstChild.offsetHeight >= limitHeight) {
const marginTop = dialogStyle.marginTop
const limitHeight = document.documentElement.clientHeight - parseInt(marginTop) - 80
if (dialog.offsetHeight >= limitHeight) {
return
}
// When emoji picker is opened, resize event has to be stopped.
const style = this.$refs.dialog.$el.firstChild.style
if (style.overflow === '' || style.overflow === 'hidden') {
const pollHeight = this.$refs.poll ? this.$refs.poll.$el.offsetHeight : 0
const spoilerHeight = this.$refs.spoiler ? this.$refs.spoiler.offsetHeight : 0
const quoteHeight = this.$refs.quote ? this.$refs.quote.$el.offsetHeight : 0
const headerHeight = 54
const footerHeight = 63
this.statusHeight =
event.height - footerHeight - headerHeight - this.$refs.preview.offsetHeight - pollHeight - spoilerHeight - quoteHeight
}
},
innerElementOpened() {
// if (open) {
// this.$refs.dialog.$el.firstChild.style.overflow = 'visible'
// } else {
// this.$refs.dialog.$el.firstChild.style.overflow = 'hidden'
// }
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,
statusHeight,
// DOM refs
previewRef,
imageRef,
pollRef,
spoilerRef,
dialogRef,
quoteRef,
// 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>

View File

@ -1,78 +1,102 @@
<template>
<div class="poll">
<ul class="poll-list">
<li class="poll-option" v-for="(option, id) in value" v-bind:key="id">
<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}`" v-model="value[id]" @input="value => updateOption(value, id)" size="small"></el-input>
<el-input :placeholder="`choice ${id}`" :modelValue="option" @input="polls[id] = $event" size="small"></el-input>
<el-button class="remove-poll" type="text" @click="removePoll(id)" size="small"><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 v-model="expiresIn" size="small" value-key="value" @change="changeExpire">
<el-option v-for="exp in expires" :key="exp.value" :label="exp.label" :value="exp"> </el-option>
<el-select :modelValue="expire" @change="$emit('update:expire', $event)" size="small" value-key="value">
<el-option v-for="exp in expiresList" :key="exp.value" :label="exp.label" :value="exp"> </el-option>
</el-select>
</div>
</template>
<script>
export default {
<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: ['value', 'defaultExpire'],
data() {
return {
expires: [
{
label: this.$t('modals.new_toot.poll.expires.5_minutes'),
value: 60 * 5
},
{
label: this.$t('modals.new_toot.poll.expires.30_minutes'),
value: 60 * 30
},
{
label: this.$t('modals.new_toot.poll.expires.1_hour'),
value: 3600
},
{
label: this.$t('modals.new_toot.poll.expires.6_hours'),
value: 3600 * 6
},
{
label: this.$t('modals.new_toot.poll.expires.1_day'),
value: 3600 * 24
},
{
label: this.$t('modals.new_toot.poll.expires.3_days'),
value: 3600 * 24 * 3
},
{
label: this.$t('modals.new_toot.poll.expires.7_days'),
value: 3600 * 24 * 7
}
],
expiresIn: null
props: {
polls: {
type: Array as PropType<Array<String>>,
default: []
},
expire: {
type: Object as PropType<Expire>,
required: true
}
},
created() {
this.expiresIn = this.defaultExpire
},
methods: {
addPoll() {
this.$emit('addPoll')
},
removePoll(id) {
this.$emit('removePoll', id)
},
updateOption(item, index) {
const newValue = [...this.value.slice(0, index), item, ...this.value.slice(index + 1)]
this.$emit('input', newValue)
},
changeExpire(exp) {
this.$emit('changeExpire', exp)
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)
}
watch(expire, (newExpire, _old) => {
ctx.emit('update:expire', newExpire)
})
watch(
polls,
(newPolls, _old) => {
ctx.emit('update:polls', newPolls)
},
{ deep: true }
)
return {
polls,
expire,
expiresList,
addPoll,
removePoll
}
}
}
})
</script>
<style lang="scss" scoped>
@ -93,7 +117,11 @@ export default {
}
.add-poll {
margin: 0 0 8px 40px;
margin: 0 4px 0 40px;
}
}
.poll :deep(.el-input__inner) {
box-shadow: none;
}
</style>

View File

@ -20,12 +20,14 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent, toRefs } from 'vue'
import DisplayStyle from '~/src/constants/displayStyle'
import FailoverImg from '@/components/atoms/FailoverImg'
import FailoverImg from '@/components/atoms/FailoverImg.vue'
import emojify from '@/utils/emojify'
import { Entity } from 'megalodon'
export default {
export default defineComponent({
new: 'quote-target',
components: {
FailoverImg
@ -40,9 +42,10 @@ export default {
default: 0
}
},
methods: {
username(account) {
switch (this.displayNameStyle) {
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)
@ -55,24 +58,30 @@ export default {
} else {
return account.acct
}
case DisplayStyle.Username.value:
default:
return account.acct
}
},
accountName(account) {
switch (this.displayNameStyle) {
}
const accountName = (account: Entity.Account) => {
switch (displayNameStyle.value) {
case DisplayStyle.DisplayNameAndUsername.value:
return `@${account.acct}`
case DisplayStyle.DisplayName.value:
case DisplayStyle.Username.value:
default:
return ''
}
},
emojiText(content, emojis) {
}
const emojiText = (content: string, emojis: Array<Entity.Emoji>) => {
return emojify(content, emojis)
}
return {
username,
accountName,
emojiText
}
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -1,15 +1,17 @@
<template>
<div class="status">
<textarea
v-model="status"
ref="status"
@paste="onPaste"
: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>
@ -19,7 +21,7 @@
v-for="(item, index) in filteredSuggestion"
:key="index"
@click="insertItem(item)"
@mouseover="highlightedIndex = index"
@mouseover="suggestHighlight(index)"
:class="{ highlighted: highlightedIndex === index }"
>
<span v-if="item.image">
@ -43,7 +45,6 @@
set="twitter"
:autoFocus="true"
@select="selectEmoji"
:custom="pickerEmojis"
:perLine="7"
:emojiSize="24"
:showPreview="false"
@ -59,23 +60,24 @@
</div>
</template>
<script>
<script lang="ts">
import 'emoji-mart-vue-fast/css/emoji-mart.css'
import data from 'emoji-mart-vue-fast/data/all.json'
import { mapState, mapGetters } from 'vuex'
import { defineComponent, computed, toRefs, ref } from 'vue'
import { Picker, EmojiIndex } from 'emoji-mart-vue-fast/src'
import suggestText from '@/utils/suggestText'
import { useStore } from '@/store'
import { MUTATION_TYPES, ACTION_TYPES } from '@/store/TimelineSpace/Modals/NewToot/Status'
const emojiIndex = new EmojiIndex(data)
export default {
export default defineComponent({
name: 'status',
components: {
Picker
},
props: {
value: {
type: String
modelValue: {
type: String,
default: ''
},
opened: {
type: Boolean,
@ -90,194 +92,147 @@ export default {
default: 120
}
},
data() {
return {
highlightedIndex: 0,
openEmojiPicker: false,
emojiIndex: emojiIndex
setup(props, ctx) {
const space = 'TimelineSpace/Modals/NewToot/Status'
const store = useStore()
const { modelValue } = toRefs(props)
const highlightedIndex = ref(0)
const statusRef = ref<HTMLTextAreaElement>()
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 openSuggest = computed({
get: () => store.state.TimelineSpace.Modals.NewToot.Status.openSuggest,
set: (value: boolean) => store.commit(`${space}/${MUTATION_TYPES.CHANGE_OPEN_SUGGEST}`, value)
})
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
})
const closeSuggest = () => {
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_SUGGEST}`)
if (openSuggest.value) {
highlightedIndex.value = 0
}
ctx.emit('suggestOpened', false)
}
},
computed: {
...mapState('TimelineSpace/Modals/NewToot/Status', {
filteredAccounts: state => state.filteredAccounts,
filteredHashtags: state => state.filteredHashtags,
openSuggest: state => state.openSuggest,
startIndex: state => state.startIndex,
matchWord: state => state.matchWord,
filteredSuggestion: state => state.filteredSuggestion
}),
...mapGetters('TimelineSpace/Modals/NewToot/Status', ['pickerEmojis']),
status: {
get: function () {
return this.value
},
set: function (value) {
this.$emit('input', value)
const suggestAccount = async (start: number, word: string) => {
try {
await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_ACCOUNT}`, { word: word, start: start })
ctx.emit('suggestOpened', true)
return true
} catch (err) {
console.log(err)
return false
}
}
},
mounted() {
// When change account, the new toot modal is recreated.
// So can not catch open event in watch.
this.$refs.status.focus()
if (this.fixCursorPos) {
this.$refs.status.setSelectionRange(0, 0)
}
},
watch: {
opened: function (newState, oldState) {
if (!oldState && newState) {
this.$nextTick(function () {
this.$refs.status.focus()
if (this.fixCursorPos) {
this.$refs.status.setSelectionRange(0, 0)
}
})
} else if (oldState && !newState) {
this.closeSuggest()
const suggestHashtag = async (start: number, word: string) => {
try {
await store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_HASHTAG}`, { word: word, start: start })
ctx.emit('suggestOpened', true)
return true
} catch (err) {
console.log(err)
return false
}
}
},
methods: {
async startSuggest(e) {
const currentValue = e.target.value
// Start suggest after user stop writing
setTimeout(async () => {
if (currentValue === this.status) {
await this.suggest(e)
}
}, 700)
},
async suggest(e) {
const suggestEmoji = async (start: number, word: string) => {
try {
store.dispatch(`${space}/${ACTION_TYPES.SUGGEST_EMOJI}`, { word: word, start: start })
ctx.emit('suggestOpened', true)
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(e.target.value, e.target.selectionStart)
const [start, word] = suggestText(target.value, target.selectionStart!)
if (!start || !word) {
this.closeSuggest()
closeSuggest()
return false
}
switch (word.charAt(0)) {
case ':':
await this.suggestEmoji(start, word)
await suggestEmoji(start, word)
return true
case '@':
await this.suggestAccount(start, word)
await suggestAccount(start, word)
return true
case '#':
await this.suggestHashtag(start, word)
await suggestHashtag(start, word)
return true
default:
return false
}
},
async suggestAccount(start, word) {
try {
await this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/suggestAccount', { word: word, start: start })
this.$emit('suggestOpened', true)
return true
} catch (err) {
console.log(err)
return false
}
},
async suggestHashtag(start, word) {
try {
await this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/suggestHashtag', { word: word, start: start })
this.$emit('suggestOpened', true)
return true
} catch (err) {
console.log(err)
return false
}
},
suggestEmoji(start, word) {
try {
this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/suggestEmoji', { word: word, start: start })
this.$emit('suggestOpened', true)
return true
} catch (err) {
this.closeSuggest()
return false
}
},
closeSuggest() {
this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/closeSuggest')
if (this.openSuggest) {
this.highlightedIndex = 0
}
this.$emit('suggestOpened', false)
},
suggestHighlight(index) {
}
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) {
this.highlightedIndex = 0
} else if (index >= this.filteredSuggestion.length) {
this.highlightedIndex = this.filteredSuggestion.length - 1
highlightedIndex.value = 0
} else if (index >= filteredSuggestion.value.length) {
highlightedIndex.value = filteredSuggestion.value.length - 1
} else {
this.highlightedIndex = index
highlightedIndex.value = index
}
},
insertItem(item) {
}
const insertItem = item => {
console.log('inserted', item.name)
if (item.code) {
const str = `${this.status.slice(0, this.startIndex - 1)}${item.code} ${this.status.slice(this.startIndex + this.matchWord.length)}`
this.status = str
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 = `${this.status.slice(0, this.startIndex - 1)}${item.name} ${this.status.slice(this.startIndex + this.matchWord.length)}`
this.status = str
const str = `${modelValue.value.slice(0, startIndex.value - 1)}${item.name} ${modelValue.value.slice(
startIndex.value + matchWord.value.length
)}`
console.log(str)
ctx.emit('update:modelValue', str)
}
this.closeSuggest()
},
selectCurrentItem() {
const item = this.filteredSuggestion[this.highlightedIndex]
this.insertItem(item)
},
onPaste(e) {
this.$emit('paste', e)
},
handleKey(event) {
const current = event.target.selectionStart
switch (event.srcKey) {
case 'up':
this.suggestHighlight(this.highlightedIndex - 1)
break
case 'down':
this.suggestHighlight(this.highlightedIndex + 1)
break
case 'enter':
this.selectCurrentItem()
break
case 'esc':
this.closeSuggest()
break
case 'left':
event.target.setSelectionRange(current - 1, current - 1)
break
case 'right':
event.target.setSelectionRange(current + 1, current + 1)
break
case 'linux':
case 'mac':
this.$emit('toot')
break
default:
return true
}
},
toggleEmojiPicker() {
this.openEmojiPicker = !this.openEmojiPicker
this.$emit('pickerOpened', this.openEmojiPicker)
},
selectEmoji(emoji) {
const current = this.$refs.status.selectionStart
closeSuggest()
}
const selectEmoji = emoji => {
const current = statusRef.value?.selectionStart
if (emoji.native) {
this.status = `${this.status.slice(0, current)}${emoji.native} ${this.status.slice(current)}`
ctx.emit('update:modelValue', `${modelValue.value.slice(0, current)}${emoji.native} ${modelValue.value.slice(current)}`)
} else {
// Custom emoji don't have natvie code
this.status = `${this.status.slice(0, current)}${emoji.name} ${this.status.slice(current)}`
ctx.emit('update:modelValue', `${modelValue.value.slice(0, current)}${emoji.name} ${modelValue.value.slice(current)}`)
}
this.hideEmojiPicker()
}
return {
emojiIndex,
highlightedIndex,
filteredAccounts,
filteredHashtags,
filteredSuggestion,
openSuggest,
startSuggest,
suggestHighlight,
insertItem,
selectEmoji
}
}
}
})
</script>
<style lang="scss">

View File

@ -10,43 +10,45 @@
</el-dialog>
</template>
<script>
import { mapState } from 'vuex'
<script lang="ts">
import { defineComponent, computed, ref } from 'vue'
import { useStore } from '@/store'
import { MUTATION_TYPES, ACTION_TYPES } from '@/store/TimelineSpace/Modals/Report'
export default {
export default defineComponent({
name: 'Report',
data() {
return {
comment: ''
setup() {
const space = 'TimelineSpace/Modals/Report'
const store = useStore()
const comment = ref<string>('')
const status = computed(() => store.state.TimelineSpace.Modals.Report.message)
const reportModal = computed({
get: () => store.state.TimelineSpace.Modals.Report.modalOpen,
set: (value: boolean) => store.commit(`${space}/${MUTATION_TYPES.CHANGE_MODAL_OPEN}`, value)
})
const closeModal = () => {
store.commit(`${space}/${MUTATION_TYPES.CHANGE_MODAL_OPEN}`, false)
}
},
computed: {
...mapState('TimelineSpace/Modals/Report', {
toot: state => state.message
}),
reportModal: {
get() {
return this.$store.state.TimelineSpace.Modals.Report.modalOpen
},
set(value) {
this.$store.commit('TimelineSpace/Modals/Report/changeModalOpen', value)
}
}
},
methods: {
closeModal() {
this.reportModal = false
},
async submit() {
this.closeModal()
await this.$store.dispatch('TimelineSpace/Modals/Report/submit', {
account_id: this.toot.account.id,
status_id: this.toot.id,
comment: this.comment
const submit = async () => {
closeModal()
await store.dispatch(`${space}/${ACTION_TYPES.SUBMIT}`, {
account_id: status.value?.account.id,
status_id: status.value?.id,
comment: comment.value
})
}
return {
comment,
reportModal,
closeModal,
submit
}
}
}
})
</script>
<style lang="scss" scoped></style>

View File

@ -1,6 +1,6 @@
<template>
<div class="shortcut">
<el-dialog :title="$t('modals.shortcut.title')" v-model="shortcutModal" width="500px" class="shortcut-modal">
<el-dialog :title="$t('modals.shortcut.title')" v-model="shortcutModal" width="500px" custom-class="shortcut-modal">
<table class="shortcuts">
<tbody>
<tr>
@ -81,20 +81,26 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'
import { MUTATION_TYPES } from '@/store/TimelineSpace/Modals/Shortcut'
export default defineComponent({
name: 'shortcut',
computed: {
shortcutModal: {
get() {
return this.$store.state.TimelineSpace.Modals.Shortcut.modalOpen
},
set(value) {
this.$store.commit('TimelineSpace/Modals/Shortcut/changeModal', value)
}
setup() {
const space = 'TimelineSpace/Modals/Shortcut'
const store = useStore()
const shortcutModal = computed({
get: () => store.state.TimelineSpace.Modals.Shortcut.modalOpen,
set: (value: boolean) => store.commit(`${space}/${MUTATION_TYPES.CHANGE_MODAL}`, value)
})
return {
shortcutModal
}
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -10,69 +10,72 @@
/>
</template>
<script>
<script lang="ts">
import { defineComponent, ref, toRefs, onMounted, watch } from 'vue'
import exifImageUrl from '@/components/utils/exifImageUrl'
export default {
export default defineComponent({
name: 'FailoverImg',
props: {
src: {
type: String,
default: '',
default: ''
},
title: {
type: String,
default: '',
default: ''
},
alt: {
type: String,
default: '',
default: ''
},
readExif: {
type: Boolean,
default: false,
default: false
},
failoverSrc: {
type: String,
default:
'data:image/png;base64,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8uyu8TbiNgXmedEw05izs7P8Fjrdad3pzMSyT9ylsyYpjwipvI2p/fBe+djEq1evGofN8qLr7PLptJane13xzkgK3Gjx2bn2BQ58QJVXf0yN6t9CeeDReNpz4Ltqy9jyucT6jwjNrtBOz5g+5rtowtnmtPB6fLPznZEU2NjhOAtMyt15/eflyw268WCm3MIG+JRA5ZbxrsPgxv9fo+/dbTnmNzY28CJaq+no3AJzh2l4mUqBafXHxppGj/r/MWM5ZWr8NG25Tf/69WuYUb180k7fp1R+bKLxCD9f7pu9jzSeMf/HOC6tP8E2/CJakwhrCpzb4cgLzCc2aq4jl1tYvjKeTqguOnsUG2L5/WLDfP9AumCdn7f86EOnYTB2LvUR5q/VmP2Yb/x7vgsi3VizaIiLRyizn+7d3nfGVmBlXjqJAlMnp6enLUebcu4693JZbJfl1c4h76jKO4X0Af8eE8vyXu1Fynlv5ZNNlVlxrP258/byK/C67i9u9UjYssDcYf3x1djEllcOIzXnP9JVmbLD/LHX9AUq5YQ2Rs4hb7OMd/Hu3buuf6v82ESbS03xZvOMIIa72H+Ve5m0iNKqj39Gh2mIjr+1tbV179696LacdMSP6vcXsVLa/H+j4zXUfEfGOkTYqcA8aZnWLu3k5KTxvHz6tXJ4j+1p0Z2NabMY/xcBl3PRljPn2LPkoSyiKitKH5nf29srK1q0d0sfEG98usFuexj1dLScPKyrll8oFNvN4eFh4zmP+LWjo6NJnPQrP8Lb8qaL8tdmT8/ETz9+/Nh4K1n8WiyiqX9b+XAj4XRvrYw1nfajbVZ27ODzd9rXZBYPFVtPuq5Y+SBf/Cjdi9z761hj281PPZtE+dOuN0zm86XlW4sXn/cU7W+hTsd+Naey0q4qLZ9Kpekt1JyzyUeenT6DsqqY/zk4OFjG41Zm4XGYMfDtRen76svD+pHfQJhe5+35OHm/RZT+RxRr9r62rNqRkN+tXUS+3gJECCIcxpAXu8zucEw4x9nZ2fn5+TDnSK+urm7JV1YiwrYGvlR694+NKV8XQYR/WZubhsAxISBCMB39W9K9V6N6q+t3pwUibNjix3yPGJiOAiIEEQIiBBGCCC0CECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBDG6D8BBgBNIRvwsstoqwAAAABJRU5ErkJggg==',
},
},
data() {
return {
loading: true,
originalSrc: this.src,
'data:image/png;base64,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8uyu8TbiNgXmedEw05izs7P8Fjrdad3pzMSyT9ylsyYpjwipvI2p/fBe+djEq1evGofN8qLr7PLptJane13xzkgK3Gjx2bn2BQ58QJVXf0yN6t9CeeDReNpz4Ltqy9jyucT6jwjNrtBOz5g+5rtowtnmtPB6fLPznZEU2NjhOAtMyt15/eflyw268WCm3MIG+JRA5ZbxrsPgxv9fo+/dbTnmNzY28CJaq+no3AJzh2l4mUqBafXHxppGj/r/MWM5ZWr8NG25Tf/69WuYUb180k7fp1R+bKLxCD9f7pu9jzSeMf/HOC6tP8E2/CJakwhrCpzb4cgLzCc2aq4jl1tYvjKeTqguOnsUG2L5/WLDfP9AumCdn7f86EOnYTB2LvUR5q/VmP2Yb/x7vgsi3VizaIiLRyizn+7d3nfGVmBlXjqJAlMnp6enLUebcu4693JZbJfl1c4h76jKO4X0Af8eE8vyXu1Fynlv5ZNNlVlxrP258/byK/C67i9u9UjYssDcYf3x1djEllcOIzXnP9JVmbLD/LHX9AUq5YQ2Rs4hb7OMd/Hu3buuf6v82ESbS03xZvOMIIa72H+Ve5m0iNKqj39Gh2mIjr+1tbV179696LacdMSP6vcXsVLa/H+j4zXUfEfGOkTYqcA8aZnWLu3k5KTxvHz6tXJ4j+1p0Z2NabMY/xcBl3PRljPn2LPkoSyiKitKH5nf29srK1q0d0sfEG98usFuexj1dLScPKyrll8oFNvN4eFh4zmP+LWjo6NJnPQrP8Lb8qaL8tdmT8/ETz9+/Nh4K1n8WiyiqX9b+XAj4XRvrYw1nfajbVZ27ODzd9rXZBYPFVtPuq5Y+SBf/Cjdi9z761hj281PPZtE+dOuN0zm86XlW4sXn/cU7W+hTsd+Naey0q4qLZ9Kpekt1JyzyUeenT6DsqqY/zk4OFjG41Zm4XGYMfDtRen76svD+pHfQJhe5+35OHm/RZT+RxRr9r62rNqRkN+tXUS+3gJECCIcxpAXu8zucEw4x9nZ2fn5+TDnSK+urm7JV1YiwrYGvlR694+NKV8XQYR/WZubhsAxISBCMB39W9K9V6N6q+t3pwUibNjix3yPGJiOAiIEEQIiBBGCCC0CECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBAQIYgQECGIEBAhiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBBECIgQRAiIEEQIiBDG6D8BBgBNIRvwsstoqwAAAABJRU5ErkJggg=='
}
},
async mounted() {
if (this.readExif) {
try {
const transformed = await exifImageUrl(this.src)
this.originalSrc = transformed
} catch (err) {
console.warn(err)
}
}
},
watch: {
src: async function (newSrc, _oldSrc) {
this.originalSrc = newSrc
if (this.readExif) {
setup(props) {
const { src, readExif, failoverSrc } = toRefs(props)
const loading = ref<boolean>(false)
const originalSrc = ref<string>(src.value)
onMounted(async () => {
if (readExif.value) {
try {
const transformed = await exifImageUrl(newSrc)
this.originalSrc = transformed
const transformed = await exifImageUrl(src.value)
originalSrc.value = transformed
} catch (err) {
console.warn(err)
}
}
},
},
methods: {
failover() {
this.originalSrc = this.failoverSrc
},
},
}
})
watch(src, async (newSrc, _oldSrc) => {
originalSrc.value = newSrc
if (readExif.value) {
try {
const transformed = await exifImageUrl(newSrc)
originalSrc.value = transformed
} catch (err) {}
}
})
const failover = () => {
originalSrc.value = failoverSrc.value
}
return {
loading,
originalSrc,
failover
}
}
})
</script>
<style lang="scss" scoped>

View File

@ -1,15 +1,10 @@
import loadImage from 'blueimp-load-image'
const parseExtension = (url) => {
const parseExtension = (url: string) => {
if (!url) {
return null
}
if (
url.match(/\.jpg$/) ||
url.match(/\.jpeg$/) ||
url.match(/\.JPG$/) ||
url.match(/\.JPEG$/)
) {
if (url.match(/\.jpg$/) || url.match(/\.jpeg$/) || url.match(/\.JPG$/) || url.match(/\.JPEG$/)) {
return 'image/jpeg'
} else if (url.match(/\.png$/) || url.match(/\.PNG$/)) {
return 'image/png'
@ -23,7 +18,7 @@ const parseExtension = (url) => {
return null
}
const exifImageUrl = (url) => {
const exifImageUrl = (url: string): Promise<string> => {
return new Promise((resolve, reject) => {
const extension = parseExtension(url)
if (!extension) {
@ -31,7 +26,7 @@ const exifImageUrl = (url) => {
}
loadImage(
url,
(canvas) => {
canvas => {
if (canvas.type === 'error') {
return reject(Error(`can not load image: ${url}`))
}
@ -41,7 +36,7 @@ const exifImageUrl = (url) => {
{
canvas: true,
meta: true,
orientation: true,
orientation: true
}
)
})

View File

@ -150,6 +150,12 @@ app.use(VueVirtualScroller)
app.use(VueResize)
app.use(i18n)
app.directive('focus', {
mounted(el) {
el.focus()
}
})
sync(store, router)
app.mount('#app')

View File

@ -21,8 +21,13 @@ const mutations: MutationTree<EditState> = {
}
}
export const ACTION_TYPES = {
FETCH_MEMBERS: 'fetchMembers',
REMOVE_ACCOUNT: 'removeAccount'
}
const actions: ActionTree<EditState, RootState> = {
fetchMembers: async ({ commit, rootState }, listId: string): Promise<Array<Entity.Account>> => {
[ACTION_TYPES.FETCH_MEMBERS]: async ({ commit, rootState }, listId: string): Promise<Array<Entity.Account>> => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
@ -33,7 +38,7 @@ const actions: ActionTree<EditState, RootState> = {
commit(MUTATION_TYPES.CHANGE_MEMBERS, res.data)
return res.data
},
removeAccount: async ({ rootState }, remove: RemoveAccountFromList): Promise<{}> => {
[ACTION_TYPES.REMOVE_ACCOUNT]: async ({ rootState }, remove: RemoveAccountFromList): Promise<{}> => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,

View File

@ -32,11 +32,17 @@ const mutations: MutationTree<AddListMemberState> = {
}
}
export const ACTION_TYPES = {
CHANGE_MODAL: 'changeModal',
SEARCH: 'search',
ADD: 'add'
}
const actions: ActionTree<AddListMemberState, RootState> = {
changeModal: ({ commit }, value: boolean) => {
[ACTION_TYPES.CHANGE_MODAL]: ({ commit }, value: boolean) => {
commit(MUTATION_TYPES.CHANGE_MODAL, value)
},
search: async ({ commit, rootState }, name: string): Promise<Array<Entity.Account>> => {
[ACTION_TYPES.SEARCH]: async ({ commit, rootState }, name: string): Promise<Array<Entity.Account>> => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
@ -47,7 +53,7 @@ const actions: ActionTree<AddListMemberState, RootState> = {
commit(MUTATION_TYPES.UPDATE_ACCOUNTS, res.data)
return res.data
},
add: async ({ state, rootState }, account: Entity.Account): Promise<{}> => {
[ACTION_TYPES.ADD]: async ({ state, rootState }, account: Entity.Account): Promise<{}> => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,

View File

@ -46,28 +46,36 @@ const mutations: MutationTree<ImageViewerState> = {
}
}
export const ACTION_TYPES = {
OPEN_MODAL: 'openModal',
CLOSE_MODAL: 'closeModal',
INCREMENT_INDEX: 'incrementIndex',
DECREMENT_INDEX: 'decrementIndex',
LOADED: 'loaded'
}
const actions: ActionTree<ImageViewerState, RootState> = {
openModal: ({ commit }, { currentIndex, mediaList }) => {
[ACTION_TYPES.OPEN_MODAL]: ({ commit }, { currentIndex, mediaList }) => {
commit(MUTATION_TYPES.CHANGE_MODAL, true)
commit(MUTATION_TYPES.CHANGE_CURRENT_INDEX, currentIndex as number)
commit(MUTATION_TYPES.CHANGE_MEDIA_LIST, mediaList as Array<Entity.Attachment>)
commit(MUTATION_TYPES.CHANGE_LOADING, true)
},
closeModal: ({ commit }) => {
[ACTION_TYPES.CLOSE_MODAL]: ({ commit }) => {
commit(MUTATION_TYPES.CHANGE_MODAL, false)
commit(MUTATION_TYPES.CHANGE_CURRENT_INDEX, -1)
commit(MUTATION_TYPES.CHANGE_MEDIA_LIST, [])
commit(MUTATION_TYPES.CHANGE_LOADING, false)
},
incrementIndex: ({ commit }) => {
[ACTION_TYPES.INCREMENT_INDEX]: ({ commit }) => {
commit(MUTATION_TYPES.INCREMENT_INDEX)
commit(MUTATION_TYPES.CHANGE_LOADING, true)
},
decrementIndex: ({ commit }) => {
[ACTION_TYPES.DECREMENT_INDEX]: ({ commit }) => {
commit(MUTATION_TYPES.DECREMENT_INDEX)
commit(MUTATION_TYPES.CHANGE_LOADING, true)
},
loaded: ({ commit }) => {
[ACTION_TYPES.LOADED]: ({ commit }) => {
commit(MUTATION_TYPES.CHANGE_LOADING, false)
}
}

View File

@ -106,19 +106,26 @@ const mutations: MutationTree<JumpState> = {
}
}
export const ACTION_TYPES = {
JUMP_CURRENT_SELECTED: 'jumpCurrentSelected',
JUMP: 'jump',
SYNC_LIST_CHANNEL: 'syncListChannel',
SYNC_TAG_CHANNEL: 'syncTagChannel'
}
const actions: ActionTree<JumpState, RootState> = {
jumpCurrentSelected: ({ state, commit, rootState }) => {
[ACTION_TYPES.JUMP_CURRENT_SELECTED]: ({ state, commit, rootState }) => {
commit(MUTATION_TYPES.CHANGE_MODAL, false)
router.push({ path: `/${rootState.TimelineSpace.account._id}/${state.selectedChannel.path}` })
},
jump: ({ commit, rootState }, channel: Channel) => {
[ACTION_TYPES.JUMP]: ({ commit, rootState }, channel: Channel) => {
commit(MUTATION_TYPES.CHANGE_MODAL, false)
router.push({ path: `/${rootState.TimelineSpace.account._id}/${channel.path}` })
},
syncListChannel: ({ commit, rootState }) => {
[ACTION_TYPES.SYNC_LIST_CHANNEL]: ({ commit, rootState }) => {
commit(MUTATION_TYPES.UPDATE_LIST_CHANNEL, rootState.TimelineSpace.SideMenu.lists)
},
syncTagChannel: ({ commit, rootState }) => {
[ACTION_TYPES.SYNC_TAG_CHANNEL]: ({ commit, rootState }) => {
commit(MUTATION_TYPES.UPDATE_TAG_CHANNEL, rootState.TimelineSpace.SideMenu.tags)
}
}

View File

@ -38,14 +38,22 @@ const mutations: MutationTree<ListMembershipState> = {
}
}
export const ACTION_TYPES = {
CHANGE_MODAL: 'changeModal',
SET_ACCOUNT: 'setAccount',
FETCH_LIST_MEMBERSHIP: 'fetchListMembership',
FETCH_LISTS: 'fetchLists',
CHANGE_BELONG_TO_LISTS: 'changeBelongToLists'
}
const actions: ActionTree<ListMembershipState, RootState> = {
changeModal: ({ commit }, value: boolean) => {
[ACTION_TYPES.CHANGE_MODAL]: ({ commit }, value: boolean) => {
commit(MUTATION_TYPES.CHANGE_MODAL, value)
},
setAccount: ({ commit }, account: Entity.Account) => {
[ACTION_TYPES.SET_ACCOUNT]: ({ commit }, account: Entity.Account) => {
commit(MUTATION_TYPES.CHANGE_ACCOUNT, account)
},
fetchListMembership: async ({ commit, rootState }, account: Entity.Account) => {
[ACTION_TYPES.FETCH_LIST_MEMBERSHIP]: async ({ commit, rootState }, account: Entity.Account) => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
@ -56,7 +64,7 @@ const actions: ActionTree<ListMembershipState, RootState> = {
commit(MUTATION_TYPES.CHANGE_BELONG_TO_LISTS, res.data)
return res.data
},
fetchLists: async ({ commit, rootState }) => {
[ACTION_TYPES.FETCH_LISTS]: async ({ commit, rootState }) => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,
@ -67,7 +75,7 @@ const actions: ActionTree<ListMembershipState, RootState> = {
commit(MUTATION_TYPES.CHANGE_LISTS, res.data)
return res.data
},
changeBelongToLists: async ({ rootState, dispatch, state }, belongToLists: Array<string>) => {
[ACTION_TYPES.CHANGE_BELONG_TO_LISTS]: async ({ rootState, dispatch, state }, belongToLists: Array<string>) => {
// Calcurate diff
const removedLists = state.belongToLists.map(l => l.id).filter(i => belongToLists.indexOf(i) === -1)
const addedLists = belongToLists.filter(i => state.belongToLists.map(l => l.id).indexOf(i) === -1)

View File

@ -26,14 +26,20 @@ const mutations: MutationTree<MuteConfirmState> = {
}
}
export const ACTION_TYPES = {
CHANGE_MODAL: 'changeModal',
CHANGE_ACCOUNT: 'changeAccount',
SUBMIT: 'submit'
}
const actions: ActionTree<MuteConfirmState, RootState> = {
changeModal: ({ commit }, value: boolean) => {
[ACTION_TYPES.CHANGE_MODAL]: ({ commit }, value: boolean) => {
commit(MUTATION_TYPES.CHANGE_MODAL, value)
},
changeAccount: ({ commit }, account: Entity.Account) => {
[ACTION_TYPES.CHANGE_ACCOUNT]: ({ commit }, account: Entity.Account) => {
commit(MUTATION_TYPES.CHANGE_ACCOUNT, account)
},
submit: async ({ state, rootState, dispatch }, notify: boolean) => {
[ACTION_TYPES.SUBMIT]: async ({ state, rootState, dispatch }, notify: boolean) => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,

View File

@ -16,7 +16,7 @@ import {
} from '@/errors/validations'
import { MyWindow } from '~/src/types/global'
const win = (window as any) as MyWindow
const win = window as any as MyWindow
type MediaDescription = {
id: string
@ -156,8 +156,27 @@ const mutations: MutationTree<NewTootState> = {
}
}
export const ACTION_TYPES = {
SETUP_LOADING: 'setupLoading',
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 actions: ActionTree<NewTootState, RootState> = {
setupLoading: ({ dispatch }) => {
[ACTION_TYPES.SETUP_LOADING]: ({ dispatch }) => {
const axiosLoading = new AxiosLoading()
axiosLoading.on('start', (_: number) => {
dispatch('startLoading')
@ -166,17 +185,17 @@ const actions: ActionTree<NewTootState, RootState> = {
dispatch('stopLoading')
})
},
startLoading: ({ commit, state }) => {
[ACTION_TYPES.START_LOADING]: ({ commit, state }) => {
if (state.modalOpen && !state.loading) {
commit(MUTATION_TYPES.CHANGE_LOADING, true)
}
},
stopLoading: ({ commit, state }) => {
[ACTION_TYPES.STOP_LOADING]: ({ commit, state }) => {
if (state.modalOpen && state.loading) {
commit(MUTATION_TYPES.CHANGE_LOADING, false)
}
},
updateMedia: async ({ rootState }, mediaDescription: MediaDescription) => {
[ACTION_TYPES.UPDATE_MEDIA]: async ({ rootState }, mediaDescription: MediaDescription) => {
if (rootState.TimelineSpace.account.accessToken === undefined || rootState.TimelineSpace.account.accessToken === null) {
throw new AuthenticationError()
}
@ -198,7 +217,7 @@ const actions: ActionTree<NewTootState, RootState> = {
throw err
})
},
postToot: async ({ state, commit, rootState, dispatch }, params: TootForm): Promise<Entity.Status> => {
[ACTION_TYPES.POST_TOOT]: async ({ state, commit, rootState, dispatch }, params: TootForm): Promise<Entity.Status> => {
if (!state.modalOpen) {
throw new NewTootModalOpen()
}
@ -287,7 +306,7 @@ const actions: ActionTree<NewTootState, RootState> = {
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, false)
})
},
openReply: ({ commit, rootState }, message: Entity.Status) => {
[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))
@ -305,18 +324,18 @@ const actions: ActionTree<NewTootState, RootState> = {
})
commit(MUTATION_TYPES.CHANGE_VISIBILITY_VALUE, value)
},
openQuote: ({ commit }, message: Entity.Status) => {
[ACTION_TYPES.OPEN_QUOTE]: ({ commit }, message: Entity.Status) => {
commit(MUTATION_TYPES.SET_QUOTE_TO, message)
commit(MUTATION_TYPES.CHANGE_MODAL, true)
},
openModal: ({ dispatch, commit, state }) => {
[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')
},
closeModal: ({ commit }) => {
[ACTION_TYPES.CLOSE_MODAL]: ({ commit }) => {
commit(MUTATION_TYPES.CHANGE_MODAL, false)
commit(MUTATION_TYPES.UPDATE_INITIAL_STATUS, '')
commit(MUTATION_TYPES.UPDATE_INITIAL_SPOILER, '')
@ -328,7 +347,7 @@ const actions: ActionTree<NewTootState, RootState> = {
commit(MUTATION_TYPES.CHANGE_SENSITIVE, false)
commit(MUTATION_TYPES.CHANGE_VISIBILITY_VALUE, Visibility.Public.value)
},
uploadImage: async ({ commit, state, rootState }, image: any) => {
[ACTION_TYPES.UPLOAD_IMAGE]: async ({ commit, state, dispatch, rootState }, image: any) => {
if (state.attachedMedias.length > 3) {
throw new NewTootAttachLength()
}
@ -348,6 +367,7 @@ const actions: ActionTree<NewTootState, RootState> = {
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 => {
@ -356,26 +376,26 @@ const actions: ActionTree<NewTootState, RootState> = {
throw err
})
},
incrementMediaCount: ({ commit, state }) => {
[ACTION_TYPES.INCREMENT_MEDIA_COUNT]: ({ commit, state }) => {
commit(MUTATION_TYPES.UPDATE_MEDIA_COUNT, state.attachedMediaCount + 1)
},
decrementMediaCount: ({ commit, state }) => {
[ACTION_TYPES.DECREMENT_MEDIA_COUNT]: ({ commit, state }) => {
commit(MUTATION_TYPES.UPDATE_MEDIA_COUNT, state.attachedMediaCount - 1)
},
resetMediaCount: ({ commit }) => {
[ACTION_TYPES.RESET_MEDIA_COUNT]: ({ commit }) => {
commit(MUTATION_TYPES.UPDATE_MEDIA_COUNT, 0)
},
removeMedia: ({ commit, dispatch }, media: Entity.Attachment) => {
[ACTION_TYPES.REMOVE_MEDIA]: ({ commit, dispatch }, media: Entity.Attachment) => {
commit(MUTATION_TYPES.REMOVE_MEDIA, media)
commit(MUTATION_TYPES.REMOVE_MEDIA_DESCRIPTION, media.id)
dispatch('decrementMediaCount')
dispatch(ACTION_TYPES.DECREMENT_MEDIA_COUNT)
},
updateHashtags: ({ commit, state }, tags: Array<Entity.Tag>) => {
[ACTION_TYPES.UPDATE_HASHTAGS]: ({ commit, state }, tags: Array<Entity.Tag>) => {
if (state.pinedHashtag && tags.length > 0) {
commit(MUTATION_TYPES.UPDATE_HASHTAGS, tags)
}
},
fetchVisibility: async ({ commit, rootState }) => {
[ACTION_TYPES.FETCH_VISIBILITY]: async ({ commit, rootState }) => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,

View File

@ -1,4 +1,5 @@
import emojidata from 'unicode-emoji-json/data-by-emoji.json'
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'
@ -7,7 +8,19 @@ 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 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
@ -27,8 +40,8 @@ export type StatusState = {
filteredHashtags: Array<SuggestHashtag>
filteredEmojis: Array<SuggestEmoji>
openSuggest: boolean
startIndex: number | null
matchWord: string | null
startIndex: number
matchWord: string
client: MegalodonInterface | null
}
@ -38,8 +51,8 @@ const state = (): StatusState => ({
filteredHashtags: [],
filteredEmojis: [],
openSuggest: false,
startIndex: null,
matchWord: null,
startIndex: 0,
matchWord: '',
client: null
})
@ -113,10 +126,10 @@ const mutations: MutationTree<StatusState> = {
[MUTATION_TYPES.CHANGE_OPEN_SUGGEST]: (state, value: boolean) => {
state.openSuggest = value
},
[MUTATION_TYPES.CHANGE_START_INDEX]: (state, index: number | null) => {
[MUTATION_TYPES.CHANGE_START_INDEX]: (state, index: number) => {
state.startIndex = index
},
[MUTATION_TYPES.CHANGE_MATCH_WORD]: (state, word: string | null) => {
[MUTATION_TYPES.CHANGE_MATCH_WORD]: (state, word: string) => {
state.matchWord = word
},
[MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS]: state => {
@ -144,8 +157,16 @@ type WordStart = {
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> = {
suggestAccount: async ({ commit, rootState, dispatch }, wordStart: WordStart) => {
[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)
@ -188,7 +209,7 @@ const actions: ActionTree<StatusState, RootState> = {
}
await Promise.all([searchCache(), searchAPI()])
},
suggestHashtag: async ({ commit, rootState, dispatch }, wordStart: WordStart) => {
[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)
@ -231,16 +252,29 @@ const actions: ActionTree<StatusState, RootState> = {
}
await Promise.all([searchCache(), searchAPI()])
},
suggestEmoji: ({ commit, rootState }, wordStart: WordStart) => {
[ACTION_TYPES.SUGGEST_EMOJI]: ({ commit, rootState }, wordStart: WordStart) => {
const { word, start } = wordStart
// Find native emojis
const filteredEmojiName: Array<string> = Object.keys(emojidata).filter((emoji: string) => `:${emojidata[emoji].name}:`.includes(word))
const filteredNativeEmoji: Array<SuggestEmoji> = filteredEmojiName.map((emoji: string) => {
const foundEmoji: EmojiMartEmoji = emojiIndex.findEmoji(word)
if (foundEmoji) {
return {
name: `:${emojidata[emoji].name}:`,
code: emoji
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 => {
@ -264,16 +298,16 @@ const actions: ActionTree<StatusState, RootState> = {
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_EMOJIS)
return filtered
},
cancelRequest: ({ state }) => {
[ACTION_TYPES.CANCEL_REQUEST]: ({ state }) => {
if (state.client) {
state.client.cancel()
}
},
closeSuggest: ({ commit, dispatch }) => {
[ACTION_TYPES.CLOSE_SUGGEST]: ({ commit, dispatch }) => {
dispatch('cancelRequest')
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, false)
commit(MUTATION_TYPES.CHANGE_START_INDEX, null)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, null)
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)

View File

@ -26,12 +26,17 @@ const mutations: MutationTree<ReportState> = {
}
}
export const ACTION_TYPES = {
OPEN_REPORT: 'openReport',
SUBMIT: 'submit'
}
const actions: ActionTree<ReportState, RootState> = {
openReport: ({ commit }, message: Entity.Status) => {
[ACTION_TYPES.OPEN_REPORT]: ({ commit }, message: Entity.Status) => {
commit(MUTATION_TYPES.CHANGE_MESSAGE, message)
commit(MUTATION_TYPES.CHANGE_MODAL_OPEN, true)
},
submit: async ({ rootState }, { account_id, status_id, comment }) => {
[ACTION_TYPES.SUBMIT]: async ({ rootState }, { account_id, status_id, comment }) => {
const client = generator(
rootState.TimelineSpace.sns,
rootState.TimelineSpace.account.baseURL,

View File

@ -1,6 +1,8 @@
const emojify = (str, customEmoji = []) => {
import { Entity } from 'megalodon'
const emojify = (str: string, customEmoji: Array<Entity.Emoji> = []): string => {
let result = str
customEmoji.map((emoji) => {
customEmoji.map(emoji => {
const reg = new RegExp(`:${emoji.shortcode}:`, 'g')
const match = result.match(reg)
if (!match) return emoji

View File

@ -1,6 +1,10 @@
// https://github.com/tootsuite/mastodon/blob/master/app/javascript/mastodon/components/autosuggest_textarea.js
const textAtCursorMatch = (str, cursorPosition, separators = ['@', '#', ':']) => {
let word
const textAtCursorMatch = (
str: string,
cursorPosition: number,
separators: Array<string> = ['@', '#', ':']
): [number | null, string | null] => {
let word: string
const left = str.slice(0, cursorPosition).search(/\S+$/)
const right = str.slice(cursorPosition).search(/\s/)