Merge pull request #3310 from h3poteto/feat/composition
Rewrite Modals with composition API
This commit is contained in:
commit
a0ba665116
|
@ -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', () => {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
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}`)
|
||||
}
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -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}`, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
const changeSelected = (channel: Channel) => {
|
||||
store.commit(`${space}/${MUTATION_TYPES.CHANGE_SELECTED}`, channel)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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,57 +142,60 @@
|
|||
</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'),
|
||||
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
|
||||
},
|
||||
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) {
|
||||
})
|
||||
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:
|
||||
|
@ -206,273 +207,301 @@ export default {
|
|||
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) {
|
||||
})
|
||||
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) {
|
||||
this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal')
|
||||
store.dispatch(`${space}/${ACTION_TYPES.OPEN_MODAL}`)
|
||||
} else {
|
||||
this.$store.dispatch('TimelineSpace/Modals/NewToot/closeModal')
|
||||
store.dispatch(`${space}/${ACTION_TYPES.CLOSE_MODAL}`)
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
newTootModal: function (newState, oldState) {
|
||||
if (!oldState && newState) {
|
||||
this.showContentWarning = this.initialSpoiler
|
||||
this.status = this.initialStatus
|
||||
this.spoiler = this.initialSpoiler
|
||||
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
|
||||
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
|
||||
})
|
||||
.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'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
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
|
||||
enableResizing.value = false
|
||||
toggle()
|
||||
nextTick(() => {
|
||||
if (openPoll.value) {
|
||||
const currentHeight = pollRef.value ? pollRef.value.$el.offsetHeight : 0
|
||||
statusHeight.value = statusHeight.value - currentHeight
|
||||
} else {
|
||||
this.statusHeight = this.statusHeight + previousHeight
|
||||
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
|
||||
}
|
||||
},
|
||||
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
|
||||
statusHeight.value = statusHeight.value + previousHeight
|
||||
}
|
||||
},
|
||||
handleResize(event) {
|
||||
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 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 = 63
|
||||
this.statusHeight =
|
||||
event.height - footerHeight - headerHeight - this.$refs.preview.offsetHeight - pollHeight - spoilerHeight - quoteHeight
|
||||
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
|
||||
}
|
||||
},
|
||||
innerElementOpened() {
|
||||
// if (open) {
|
||||
// this.$refs.dialog.$el.firstChild.style.overflow = 'visible'
|
||||
// } else {
|
||||
// this.$refs.dialog.$el.firstChild.style.overflow = 'hidden'
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
methods: {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -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: [
|
||||
props: {
|
||||
polls: {
|
||||
type: Array as PropType<Array<String>>,
|
||||
default: []
|
||||
},
|
||||
expire: {
|
||||
type: Object as PropType<Expire>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props, ctx) {
|
||||
const i18n = useI18next()
|
||||
const { expire, polls } = toRefs(props)
|
||||
const expiresList = reactive<Array<Expire>>([
|
||||
{
|
||||
label: this.$t('modals.new_toot.poll.expires.5_minutes'),
|
||||
label: i18n.t('modals.new_toot.poll.expires.5_minutes'),
|
||||
value: 60 * 5
|
||||
},
|
||||
{
|
||||
label: this.$t('modals.new_toot.poll.expires.30_minutes'),
|
||||
label: i18n.t('modals.new_toot.poll.expires.30_minutes'),
|
||||
value: 60 * 30
|
||||
},
|
||||
{
|
||||
label: this.$t('modals.new_toot.poll.expires.1_hour'),
|
||||
label: i18n.t('modals.new_toot.poll.expires.1_hour'),
|
||||
value: 3600
|
||||
},
|
||||
{
|
||||
label: this.$t('modals.new_toot.poll.expires.6_hours'),
|
||||
label: i18n.t('modals.new_toot.poll.expires.6_hours'),
|
||||
value: 3600 * 6
|
||||
},
|
||||
{
|
||||
label: this.$t('modals.new_toot.poll.expires.1_day'),
|
||||
label: i18n.t('modals.new_toot.poll.expires.1_day'),
|
||||
value: 3600 * 24
|
||||
},
|
||||
{
|
||||
label: this.$t('modals.new_toot.poll.expires.3_days'),
|
||||
label: i18n.t('modals.new_toot.poll.expires.3_days'),
|
||||
value: 3600 * 24 * 3
|
||||
},
|
||||
{
|
||||
label: this.$t('modals.new_toot.poll.expires.7_days'),
|
||||
label: i18n.t('modals.new_toot.poll.expires.7_days'),
|
||||
value: 3600 * 24 * 7
|
||||
}
|
||||
],
|
||||
expiresIn: null
|
||||
])
|
||||
|
||||
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)
|
||||
},
|
||||
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)
|
||||
}
|
||||
{ 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
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)
|
||||
})
|
||||
} else if (oldState && !newState) {
|
||||
this.closeSuggest()
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
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
|
||||
}
|
||||
}, 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
|
||||
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)
|
||||
}
|
||||
},
|
||||
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
|
||||
}, 700)
|
||||
}
|
||||
},
|
||||
closeSuggest() {
|
||||
this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/closeSuggest')
|
||||
if (this.openSuggest) {
|
||||
this.highlightedIndex = 0
|
||||
}
|
||||
this.$emit('suggestOpened', false)
|
||||
},
|
||||
suggestHighlight(index) {
|
||||
|
||||
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
|
||||
closeSuggest()
|
||||
}
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.openEmojiPicker = !this.openEmojiPicker
|
||||
this.$emit('pickerOpened', this.openEmojiPicker)
|
||||
},
|
||||
selectEmoji(emoji) {
|
||||
const current = this.$refs.status.selectionStart
|
||||
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)}`
|
||||
}
|
||||
this.hideEmojiPicker()
|
||||
ctx.emit('update:modelValue', `${modelValue.value.slice(0, current)}${emoji.name} ${modelValue.value.slice(current)}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
emojiIndex,
|
||||
highlightedIndex,
|
||||
filteredAccounts,
|
||||
filteredHashtags,
|
||||
filteredSuggestion,
|
||||
openSuggest,
|
||||
startSuggest,
|
||||
suggestHighlight,
|
||||
insertItem,
|
||||
selectEmoji
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
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(this.src)
|
||||
this.originalSrc = transformed
|
||||
const transformed = await exifImageUrl(src.value)
|
||||
originalSrc.value = transformed
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
src: async function (newSrc, _oldSrc) {
|
||||
this.originalSrc = newSrc
|
||||
if (this.readExif) {
|
||||
})
|
||||
|
||||
watch(src, async (newSrc, _oldSrc) => {
|
||||
originalSrc.value = newSrc
|
||||
if (readExif.value) {
|
||||
try {
|
||||
const transformed = await exifImageUrl(newSrc)
|
||||
this.originalSrc = transformed
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
originalSrc.value = transformed
|
||||
} catch (err) {}
|
||||
}
|
||||
})
|
||||
|
||||
const failover = () => {
|
||||
originalSrc.value = failoverSrc.value
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
originalSrc,
|
||||
failover
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
failover() {
|
||||
this.originalSrc = this.failoverSrc
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
})
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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/)
|
Loading…
Reference in New Issue