Whalebird-desktop-client-ma.../src/renderer/components/TimelineSpace/Modals/NewToot.vue

585 lines
16 KiB
Vue
Raw Normal View History

2018-03-12 17:42:47 +01:00
<template>
<el-dialog
:title="$t('modals.new_toot.title')"
2018-03-12 17:42:47 +01:00
:visible.sync="newTootModal"
:before-close="closeConfirm"
2018-03-12 17:42:47 +01:00
width="400px"
class="new-toot-modal"
>
2018-10-21 04:17:59 +02:00
<el-form v-on:submit.prevent="toot" role="form">
<div class="spoiler" v-show="showContentWarning">
2018-11-29 17:39:48 +01:00
<div class="el-input">
<input type="text" class="el-input__inner" :placeholder="$t('modals.new_toot.cw')" v-model="spoiler" v-shortkey.avoid />
</div>
</div>
<Status
v-model="status"
:opened="newTootModal"
:fixCursorPos="hashtagInserting"
:height="statusHeight"
@paste="onPaste"
@toot="toot"
/>
2018-03-12 17:42:47 +01:00
</el-form>
2019-07-16 18:36:19 +02:00
<Poll
v-if="openPoll"
v-model="polls"
@addPoll="addPoll"
@removePoll="removePoll"
:defaultExpire="pollExpire"
@changeExpire="changeExpire"
ref="poll"
2019-07-16 18:36:19 +02:00
></Poll>
<div class="preview" ref="preview">
2018-04-09 14:10:25 +02:00
<div class="image-wrapper" v-for="media in attachedMedias" v-bind:key="media.id">
<img :src="media.preview_url" class="preview-image" />
2018-11-19 15:56:49 +01:00
<el-button type="text" @click="removeAttachment(media)" class="remove-image"><icon name="times-circle"></icon></el-button>
<textarea
maxlength="420"
class="image-description"
:placeholder="$t('modals.new_toot.description')"
:value="mediaDescriptions[media.id]"
@input="updateDescription(media.id, $event.target.value)"
v-shortkey="{ left: ['arrowleft'], right: ['arrowright'] }"
@shortkey="handleDescriptionKey"
role="textbox"
contenteditable="true"
aria-multiline="true"
>
</textarea>
</div>
</div>
<div slot="footer" class="dialog-footer">
<div class="upload-image">
<el-button size="small" type="text" @click="selectImage" :title="$t('modals.new_toot.footer.add_image')">
2018-08-21 02:18:11 +02:00
<icon name="camera"></icon>
</el-button>
<input name="image" type="file" class="image-input" ref="image" @change="onChangeImage" :key="attachedMediaId" />
</div>
<div class="poll">
<el-button size="small" type="text" @click="togglePollForm" :title="$t('modals.new_toot.footer.poll')">
<icon name="poll"></icon>
</el-button>
</div>
<div class="privacy">
<el-dropdown trigger="click" @command="changeVisibility">
<el-button size="small" type="text" :title="$t('modals.new_toot.footer.change_visibility')">
2018-08-21 02:18:11 +02:00
<icon :name="visibilityIcon"></icon>
</el-button>
<el-dropdown-menu slot="dropdown">
2018-08-10 17:59:48 +02:00
<el-dropdown-item :command="visibilityList.Public.value">
<icon name="globe" class="privacy-icon"></icon>
{{ $t(visibilityList.Public.name) }}
2018-08-10 17:59:48 +02:00
</el-dropdown-item>
<el-dropdown-item :command="visibilityList.Unlisted.value">
<icon name="unlock" class="privacy-icon"></icon>
{{ $t(visibilityList.Unlisted.name) }}
2018-08-10 17:59:48 +02:00
</el-dropdown-item>
<el-dropdown-item :command="visibilityList.Private.value">
<icon name="lock" class="privacy-icon"></icon>
{{ $t(visibilityList.Private.name) }}
2018-08-10 17:59:48 +02:00
</el-dropdown-item>
<el-dropdown-item :command="visibilityList.Direct.value">
<icon name="envelope" class="privacy-icon" scale="0.8"></icon>
{{ $t(visibilityList.Direct.name) }}
2018-08-10 17:59:48 +02:00
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="sensitive" v-show="attachedMedias.length > 0">
<el-button
size="small"
type="text"
@click="changeSensitive"
:title="$t('modals.new_toot.footer.change_sensitive')"
:aria-pressed="sensitive"
>
2018-12-08 01:21:44 +01:00
<icon name="eye-slash" v-show="!sensitive"></icon>
<icon name="eye" v-show="sensitive"></icon>
</el-button>
</div>
<div class="content-warning">
<el-button
size="small"
type="text"
@click="showContentWarning = !showContentWarning"
:title="$t('modals.new_toot.footer.add_cw')"
:class="showContentWarning ? '' : 'clickable'"
:aria-pressed="showContentWarning"
>
2018-09-18 15:19:11 +02:00
<span class="cw-text">CW</span>
</el-button>
</div>
<div class="pined-hashtag">
<el-button
size="small"
type="text"
@click="pinedHashtag = !pinedHashtag"
:title="$t('modals.new_toot.footer.pined_hashtag')"
:class="pinedHashtag ? '' : 'clickable'"
:aria-pressed="pinedHashtag"
>
<icon name="hashtag"></icon>
</el-button>
</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>
<el-button class="toot-action" size="small" @click="closeConfirm(close)">{{ $t('modals.new_toot.cancel') }}</el-button>
<el-button class="toot-action" size="small" type="primary" @click="toot" :loading="blockSubmit">{{
$t('modals.new_toot.toot')
}}</el-button>
</div>
2018-03-29 08:49:39 +02:00
<div class="clearfix"></div>
</div>
<resize-observer @notify="handleResize" />
2018-03-12 17:42:47 +01:00
</el-dialog>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import Visibility from '~/src/constants/visibility'
import Status from './NewToot/Status'
2019-07-15 14:38:03 +02:00
import Poll from './NewToot/Poll'
import { NewTootTootLength, NewTootAttachLength, NewTootModalOpen, NewTootBlockSubmit, NewTootPollInvalid } from '@/errors/validations'
2018-03-12 17:42:47 +01:00
export default {
name: 'new-toot',
components: {
2019-07-15 14:38:03 +02:00
Status,
Poll
},
data() {
return {
status: '',
spoiler: '',
2018-08-10 17:59:48 +02:00
showContentWarning: false,
visibilityList: Visibility,
openPoll: false,
polls: [],
2019-07-16 18:36:19 +02:00
pollExpire: {
label: this.$t('modals.new_toot.poll.expires.1_day'),
value: 3600 * 24
},
statusHeight: 240
}
},
2018-03-12 17:42:47 +01:00
computed: {
...mapState('TimelineSpace/Modals/NewToot', {
replyToId: state => {
if (state.replyToMessage !== null) {
return state.replyToMessage.id
} else {
return null
}
},
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,
2018-11-17 19:27:58 +01:00
initialSpoiler: state => state.initialSpoiler,
visibilityIcon: state => {
switch (state.visibility) {
2018-08-01 16:41:05 +02:00
case Visibility.Public.value:
return 'globe'
2018-08-01 16:41:05 +02:00
case Visibility.Unlisted.value:
return 'unlock'
2018-08-01 16:41:05 +02:00
case Visibility.Private.value:
return 'lock'
2018-08-01 16:41:05 +02:00
case Visibility.Direct.value:
return 'envelope'
default:
return 'globe'
}
},
loading: state => state.loading
}),
...mapState('TimelineSpace', {
tootMax: state => state.tootMax
}),
...mapGetters('TimelineSpace/Modals/NewToot', ['hashtagInserting']),
2018-03-12 17:42:47 +01:00
newTootModal: {
get() {
return this.$store.state.TimelineSpace.Modals.NewToot.modalOpen
2018-03-12 17:42:47 +01:00
},
set(value) {
if (value) {
this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal')
} else {
this.$store.dispatch('TimelineSpace/Modals/NewToot/closeModal')
}
2018-03-12 17:42:47 +01:00
}
},
pinedHashtag: {
get() {
return this.$store.state.TimelineSpace.Modals.NewToot.pinedHashtag
},
set(value) {
this.$store.commit('TimelineSpace/Modals/NewToot/changePinedHashtag', value)
}
2018-03-12 17:42:47 +01:00
}
2018-03-13 00:41:03 +01:00
},
created() {
this.$store.dispatch('TimelineSpace/Modals/NewToot/setupLoading')
},
watch: {
newTootModal: function (newState, oldState) {
if (!oldState && newState) {
2018-11-17 19:27:58 +01:00
this.showContentWarning = this.initialSpoiler
this.status = this.initialStatus
2018-11-17 19:27:58 +01:00
this.spoiler = this.initialSpoiler
}
}
},
2018-03-13 00:41:03 +01:00
methods: {
close() {
this.filteredAccount = []
this.openPoll = false
this.polls = []
2019-07-16 18:36:19 +02:00
this.pollExpire = {
label: this.$t('modals.new_toot.poll.expires.1_day'),
value: 3600 * 24
}
2019-05-27 15:56:54 +02:00
this.$store.dispatch('TimelineSpace/Modals/NewToot/resetMediaCount')
this.$store.dispatch('TimelineSpace/Modals/NewToot/closeModal')
},
async toot() {
const form = {
status: this.status,
2019-07-16 16:56:00 +02:00
spoiler: this.spoiler,
2019-07-16 18:36:19 +02:00
polls: this.polls,
pollExpireSeconds: this.pollExpire.value
2018-03-29 06:16:15 +02:00
}
2018-04-09 09:49:41 +02:00
try {
2019-06-03 15:27:50 +02:00
const status = await this.$store.dispatch('TimelineSpace/Modals/NewToot/postToot', form)
this.$store.dispatch('TimelineSpace/Modals/NewToot/updateHashtags', status.tags)
this.close()
} catch (err) {
console.error(err)
if (err instanceof NewTootTootLength) {
this.$message({
message: this.$t('validation.new_toot.toot_length', { min: 1, max: this.tootMax }),
type: 'error'
})
} else if (err instanceof NewTootAttachLength) {
this.$message({
message: this.$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'),
type: 'error'
})
} else if (err instanceof NewTootModalOpen || err instanceof NewTootBlockSubmit) {
// Nothing
} else {
this.$message({
message: this.$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) {
return
}
const file = e.target.files.item(0)
if (!file.type.includes('image') && !file.type.includes('video')) {
this.$message({
2018-08-13 08:27:53 +02:00
message: this.$t('validation.new_toot.attach_image'),
type: 'error'
})
return
}
this.updateImage(file)
},
onPaste(e) {
const mimeTypes = window.clipboard.availableFormats().filter(type => type.startsWith('image'))
2018-08-20 05:22:35 +02:00
if (mimeTypes.length === 0) {
return
}
e.preventDefault()
const image = window.clipboard.readImage()
2018-08-20 05:22:35 +02:00
let data
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) {
2019-05-27 15:56:54 +02:00
this.$store.dispatch('TimelineSpace/Modals/NewToot/incrementMediaCount')
this.$store
.dispatch('TimelineSpace/Modals/NewToot/uploadImage', file)
.then(() => {
this.statusHeight = this.statusHeight - this.$refs.preview.offsetHeight
})
.catch(() => {
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
})
2018-03-29 08:49:39 +02:00
},
changeVisibility(level) {
2018-08-10 17:59:48 +02:00
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) {
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')
})
.then(_ => {
done()
})
.catch(_ => {})
}
},
handleDescriptionKey(event) {
const current = event.target.selectionStart
switch (event.srcKey) {
case 'left':
event.target.setSelectionRange(current - 1, current - 1)
break
case 'right':
event.target.setSelectionRange(current + 1, current + 1)
break
default:
return true
}
},
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 toggle = () => {
this.openPoll = !this.openPoll
if (this.openPoll) {
this.polls = ['', '']
} else {
this.polls = []
}
}
await toggle()
console.log(this.$refs.poll)
if (this.openPoll) {
this.statusHeight = this.statusHeight - this.$refs.poll.$el.offsetHeight
} else {
this.statusHeight = this.statusHeight + previousHeight
}
},
addPoll() {
this.polls.push('')
},
removePoll(id) {
this.polls.splice(id, 1)
2019-07-16 18:36:19 +02:00
},
changeExpire(obj) {
this.pollExpire = obj
},
handleResize(event) {
const pollHeight = this.$refs.poll ? this.$refs.poll.$el.offsetHeight : 0
this.statusHeight = event.height - 63 - 54 - this.$refs.preview.offsetHeight - pollHeight
2018-03-13 00:41:03 +01:00
}
2018-03-12 17:42:47 +01:00
}
}
</script>
<style lang="scss" scoped>
.new-toot-modal /deep/ {
.el-dialog {
background-color: #f2f6fc;
overflow: hidden;
resize: both;
}
2018-03-12 17:42:47 +01:00
.el-dialog__header {
background-color: #4a5664;
.el-dialog__title {
color: #ebeef5;
}
}
.el-dialog__body {
padding: 0;
.spoiler {
box-sizing: border-box;
padding: 4px 0;
background-color: #4a5664;
input {
border-radius: 0;
&::placeholder {
color: #c0c4cc;
}
}
}
.preview {
box-sizing: border-box;
2018-11-19 15:56:49 +01:00
display: flex;
flex-direction: row;
flex-wrap: wrap;
.image-wrapper {
position: relative;
2018-11-19 15:56:49 +01:00
flex: 1 1 0;
min-width: 40%;
height: 150px;
margin: 4px;
.preview-image {
2018-11-19 15:56:49 +01:00
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
border-radius: 8px;
}
.image-description {
position: absolute;
2018-11-19 19:17:38 +01:00
left: 0;
bottom: 0;
2018-11-19 15:56:49 +01:00
width: 100%;
box-sizing: border-box;
border: 0;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0, rgba(0, 0, 0, 0.35) 80%, transparent);
font-size: var(--font-base-size);
color: #fff;
opacity: 1;
resize: none;
overflow: scroll;
2018-11-19 15:56:49 +01:00
&::placeholder {
color: #c0c4cc;
}
}
.remove-image {
position: absolute;
2018-11-19 15:56:49 +01:00
top: 2px;
left: 2px;
2018-03-29 08:49:39 +02:00
padding: 0;
cursor: pointer;
2018-11-19 15:56:49 +01:00
font-size: 1.5rem;
.fa-icon {
font-size: 0.9rem;
2018-11-19 15:56:49 +01:00
width: auto;
height: 1em;
max-width: 100%;
max-height: 100%;
}
}
}
}
2018-03-12 17:42:47 +01:00
}
.el-dialog__footer {
background-color: #f2f6fc;
font-size: var(--base-font-size);
padding-bottom: 0;
margin-bottom: 20px;
2018-03-13 02:52:28 +01:00
.upload-image {
text-align: left;
2018-03-29 08:49:39 +02:00
float: left;
.image-input {
display: none;
}
}
.poll {
float: left;
margin-left: 8px;
}
.privacy {
float: left;
margin-left: 8px;
}
.sensitive {
float: left;
margin-left: 8px;
}
.content-warning {
float: left;
margin-left: 8px;
2018-09-18 15:19:11 +02:00
.cw-text {
font-weight: 800;
2018-09-18 15:19:11 +02:00
line-height: 18px;
}
}
.pined-hashtag {
float: left;
margin-left: 8px;
}
.clickable {
color: #909399;
}
.info {
display: flex;
justify-content: flex-end;
align-items: center;
.loading {
width: 18px;
margin-right: 4px;
}
.text-count {
padding-right: 10px;
color: #909399;
}
2018-03-13 02:52:28 +01:00
}
2018-11-17 19:27:58 +01:00
.toot-action {
font-size: var(--base-font-size);
margin-top: 2px;
margin-bottom: 2px;
}
2018-03-12 17:42:47 +01:00
}
}
.privacy-icon {
margin-right: 4px;
}
2018-03-12 17:42:47 +01:00
</style>