Merge pull request #4016 from h3poteto/iss-3771
refs #3771 Add compose window to footer
This commit is contained in:
commit
29100da80a
|
@ -294,7 +294,7 @@
|
||||||
"add_image": "Add images",
|
"add_image": "Add images",
|
||||||
"poll": "Add a poll",
|
"poll": "Add a poll",
|
||||||
"change_visibility": "Change visibility",
|
"change_visibility": "Change visibility",
|
||||||
"change_sensitive": "Change sensitive",
|
"change_sensitive": "Mark media as sensitive",
|
||||||
"add_cw": "Add content warning",
|
"add_cw": "Add content warning",
|
||||||
"pined_hashtag": "Pin the hashtag"
|
"pined_hashtag": "Pin the hashtag"
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,13 @@
|
||||||
<header class="header" style="-webkit-app-region: drag">
|
<header class="header" style="-webkit-app-region: drag">
|
||||||
<header-menu></header-menu>
|
<header-menu></header-menu>
|
||||||
</header>
|
</header>
|
||||||
<contents></contents>
|
<div class="contents-wrapper" ref="contentsRef">
|
||||||
|
<contents />
|
||||||
|
</div>
|
||||||
|
<div class="compose-wrapper">
|
||||||
|
<compose />
|
||||||
|
<resize-observer @notify="composeResized" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<modals></modals>
|
<modals></modals>
|
||||||
<receive-drop v-show="droppableVisible"></receive-drop>
|
<receive-drop v-show="droppableVisible"></receive-drop>
|
||||||
|
@ -26,6 +32,7 @@ import { useI18next } from 'vue3-i18next'
|
||||||
import SideMenu from './TimelineSpace/SideMenu.vue'
|
import SideMenu from './TimelineSpace/SideMenu.vue'
|
||||||
import HeaderMenu from './TimelineSpace/HeaderMenu.vue'
|
import HeaderMenu from './TimelineSpace/HeaderMenu.vue'
|
||||||
import Contents from './TimelineSpace/Contents.vue'
|
import Contents from './TimelineSpace/Contents.vue'
|
||||||
|
import Compose from './TimelineSpace/Compose.vue'
|
||||||
import Modals from './TimelineSpace/Modals.vue'
|
import Modals from './TimelineSpace/Modals.vue'
|
||||||
import Mousetrap from 'mousetrap'
|
import Mousetrap from 'mousetrap'
|
||||||
import ReceiveDrop from './TimelineSpace/ReceiveDrop.vue'
|
import ReceiveDrop from './TimelineSpace/ReceiveDrop.vue'
|
||||||
|
@ -42,7 +49,7 @@ import { ACTION_TYPES as NEW_TOOT_ACTION } from '@/store/TimelineSpace/Modals/Ne
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'timeline-space',
|
name: 'timeline-space',
|
||||||
components: { SideMenu, HeaderMenu, Modals, Contents, ReceiveDrop },
|
components: { SideMenu, HeaderMenu, Modals, Contents, ReceiveDrop, Compose },
|
||||||
setup() {
|
setup() {
|
||||||
const space = 'TimelineSpace'
|
const space = 'TimelineSpace'
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
@ -51,6 +58,7 @@ export default defineComponent({
|
||||||
|
|
||||||
const dropTarget = ref<any>(null)
|
const dropTarget = ref<any>(null)
|
||||||
const droppableVisible = ref<boolean>(false)
|
const droppableVisible = ref<boolean>(false)
|
||||||
|
const contentsRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const loading = computed(() => store.state.TimelineSpace.loading)
|
const loading = computed(() => store.state.TimelineSpace.loading)
|
||||||
const collapse = computed(() => store.state.TimelineSpace.SideMenu.collapse)
|
const collapse = computed(() => store.state.TimelineSpace.SideMenu.collapse)
|
||||||
|
@ -155,10 +163,18 @@ export default defineComponent({
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const composeResized = (event: { width: number; height: number }) => {
|
||||||
|
if (contentsRef.value) {
|
||||||
|
contentsRef.value.style.setProperty('height', `calc(100% - ${event.height}px)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
collapse,
|
collapse,
|
||||||
droppableVisible
|
droppableVisible,
|
||||||
|
composeResized,
|
||||||
|
contentsRef
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -169,6 +185,12 @@ export default defineComponent({
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compose-wrapper {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0 12px 18px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
margin-left: 180px;
|
margin-left: 180px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -0,0 +1,512 @@
|
||||||
|
<template>
|
||||||
|
<div class="compose">
|
||||||
|
<el-form :model="form" class="compose-form">
|
||||||
|
<el-input v-model="form.spoiler" class="spoiler" :placeholder="$t('modals.new_toot.cw')" v-if="cw" />
|
||||||
|
<el-input
|
||||||
|
v-model="form.status"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 2 }"
|
||||||
|
:placeholder="$t('modals.new_toot.status')"
|
||||||
|
ref="statusRef"
|
||||||
|
/>
|
||||||
|
<div class="preview" ref="previewRef">
|
||||||
|
<div class="image-wrapper" v-for="media in attachments" :key="media.id">
|
||||||
|
<img :src="media.preview_url" class="preview-image" />
|
||||||
|
<el-button class="remove-image" link @click="removeAttachment(media)"><font-awesome-icon icon="circle-xmark" /></el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nsfw" v-if="attachments.length > 0">
|
||||||
|
<el-checkbox v-model="nsfw">{{ $t('modals.new_toot.footer.change_sensitive') }}</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="poll" v-if="poll.options.length > 0">
|
||||||
|
<ul class="options-list">
|
||||||
|
<li class="option" v-for="(option, index) in poll.options" :key="index">
|
||||||
|
<el-radio :disabled="true" :label="index">
|
||||||
|
<el-input :placeholder="`Choice ${index}`" v-model="poll.options[index]" size="small"></el-input>
|
||||||
|
<el-button class="remove-poll" link size="small" @click="removePollOption(index)"
|
||||||
|
><font-awesome-icon icon="xmark"
|
||||||
|
/></el-button>
|
||||||
|
</el-radio>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<el-button class="add-poll" type="info" size="small" @click="addPollOption">{{ $t('modals.new_toot.poll.add_choice') }}</el-button>
|
||||||
|
<el-select v-model="poll.expires_in" size="small" value-key="value">
|
||||||
|
<el-option v-for="exp in expiresList" :key="exp.value" :label="exp.label" :value="exp.value"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-footer">
|
||||||
|
<el-button-group class="tool-buttons">
|
||||||
|
<el-button link size="default" @click="selectImage">
|
||||||
|
<font-awesome-icon icon="camera" />
|
||||||
|
</el-button>
|
||||||
|
<input name="image" type="file" class="image-input" ref="imageRef" @change="onChangeImage" />
|
||||||
|
<el-button link size="default" @click="togglePoll">
|
||||||
|
<font-awesome-icon icon="square-poll-horizontal" />
|
||||||
|
</el-button>
|
||||||
|
<div>
|
||||||
|
<el-dropdown trigger="click" @command="changeVisibility">
|
||||||
|
<el-button size="default" link :title="$t('modals.new_toot.footer.change_visibility')">
|
||||||
|
<font-awesome-icon :icon="visibilityIcon" />
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item :command="visibilityList.Public.key">
|
||||||
|
<font-awesome-icon icon="globe" class="privacy-icon" />
|
||||||
|
{{ $t(visibilityList.Public.name) }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item :command="visibilityList.Unlisted.key">
|
||||||
|
<font-awesome-icon icon="unlock" class="privacy-icon" />
|
||||||
|
{{ $t(visibilityList.Unlisted.name) }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item :command="visibilityList.Private.key">
|
||||||
|
<font-awesome-icon icon="lock" class="privacy-icon" />
|
||||||
|
{{ $t(visibilityList.Private.name) }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item :command="visibilityList.Direct.key">
|
||||||
|
<font-awesome-icon icon="envelope" class="privacy-icon" size="sm" />
|
||||||
|
{{ $t(visibilityList.Direct.name) }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<el-popover placement="top" width="0" :visible="emojiVisible" popper-class="new-toot-emoji-picker">
|
||||||
|
<picker
|
||||||
|
v-if="emojiData !== null"
|
||||||
|
:data="emojiData"
|
||||||
|
set="twitter"
|
||||||
|
:autoFocus="true"
|
||||||
|
@select="selectEmoji"
|
||||||
|
:perLine="7"
|
||||||
|
:emojiSize="24"
|
||||||
|
:showPreview="false"
|
||||||
|
:emojiTooltip="true"
|
||||||
|
/>
|
||||||
|
<template #reference>
|
||||||
|
<el-button link size="default" @click="emojiVisible = !emojiVisible">
|
||||||
|
<font-awesome-icon :icon="['far', 'face-smile']" />
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
<el-button link size="default" @click="cw = !cw"> CW </el-button>
|
||||||
|
</el-button-group>
|
||||||
|
<div class="actions-group">
|
||||||
|
<span>500</span>
|
||||||
|
<el-button type="primary" @click="post" :loading="loading"> {{ $t('modals.new_toot.toot') }} </el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, computed, ref, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import generator, { Entity, MegalodonInterface } from 'megalodon'
|
||||||
|
import emojiDefault from 'emoji-mart-vue-fast/data/all.json'
|
||||||
|
import { Picker, EmojiIndex } from 'emoji-mart-vue-fast/src'
|
||||||
|
import { useI18next } from 'vue3-i18next'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
import { MyWindow } from '~/src/types/global'
|
||||||
|
import { LocalAccount } from '~/src/types/localAccount'
|
||||||
|
import { LocalServer } from '~/src/types/localServer'
|
||||||
|
import visibilityList from '~/src/constants/visibility'
|
||||||
|
import { MUTATION_TYPES } from '@/store/TimelineSpace/Compose'
|
||||||
|
|
||||||
|
type Expire = {
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Compose',
|
||||||
|
components: { Picker },
|
||||||
|
setup() {
|
||||||
|
const route = useRoute()
|
||||||
|
const store = useStore()
|
||||||
|
const i18n = useI18next()
|
||||||
|
const space = 'TimelineSpace/Compose'
|
||||||
|
const win = (window as any) as MyWindow
|
||||||
|
|
||||||
|
const id = computed(() => parseInt(route.params.id as string))
|
||||||
|
const userAgent = computed(() => store.state.App.userAgent)
|
||||||
|
const visibilityIcon = computed(() => {
|
||||||
|
switch (visibility.value) {
|
||||||
|
case visibilityList.Public.key:
|
||||||
|
return 'globe'
|
||||||
|
case visibilityList.Unlisted.key:
|
||||||
|
return 'unlock'
|
||||||
|
case visibilityList.Private.key:
|
||||||
|
return 'lock'
|
||||||
|
case visibilityList.Direct.key:
|
||||||
|
return 'envelope'
|
||||||
|
default:
|
||||||
|
return 'globe'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const expiresList = reactive<Array<Expire>>([
|
||||||
|
{
|
||||||
|
label: i18n.t('modals.new_toot.poll.expires.5_minutes'),
|
||||||
|
value: 60 * 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t('modals.new_toot.poll.expires.30_minutes'),
|
||||||
|
value: 60 * 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t('modals.new_toot.poll.expires.1_hour'),
|
||||||
|
value: 3600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t('modals.new_toot.poll.expires.6_hours'),
|
||||||
|
value: 3600 * 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t('modals.new_toot.poll.expires.1_day'),
|
||||||
|
value: 3600 * 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t('modals.new_toot.poll.expires.3_days'),
|
||||||
|
value: 3600 * 24 * 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t('modals.new_toot.poll.expires.7_days'),
|
||||||
|
value: 3600 * 24 * 7
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const emojiData = ref<EmojiIndex | null>(null)
|
||||||
|
const client = ref<MegalodonInterface | null>(null)
|
||||||
|
const form = reactive({
|
||||||
|
status: '',
|
||||||
|
spoiler: ''
|
||||||
|
})
|
||||||
|
const attachments = ref<Array<Entity.Attachment | Entity.AsyncAttachment>>([])
|
||||||
|
const cw = ref<boolean>(false)
|
||||||
|
const visibility = ref(visibilityList.Public.key)
|
||||||
|
const nsfw = ref<boolean>(false)
|
||||||
|
const inReplyTo = computed(() => store.state.TimelineSpace.Compose.inReplyTo)
|
||||||
|
const poll = reactive<{ options: Array<string>; expires_in: number }>({
|
||||||
|
options: [],
|
||||||
|
expires_in: 86400
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref<boolean>(false)
|
||||||
|
const emojiVisible = ref<boolean>(false)
|
||||||
|
const imageRef = ref<any>(null)
|
||||||
|
const statusRef = ref<any>(null)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const [a, s]: [LocalAccount, LocalServer] = await win.ipcRenderer.invoke('get-local-account', id.value)
|
||||||
|
const c = generator(s.sns, s.baseURL, a.accessToken, userAgent.value)
|
||||||
|
client.value = c
|
||||||
|
|
||||||
|
const res = await c.getInstanceCustomEmojis()
|
||||||
|
const customEmojis = res.data
|
||||||
|
.map(emoji => {
|
||||||
|
return {
|
||||||
|
name: `:${emoji.shortcode}:`,
|
||||||
|
image: emoji.url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((e, i, array) => {
|
||||||
|
return array.findIndex(ar => e.name === ar.name) === i
|
||||||
|
})
|
||||||
|
.map(e => ({
|
||||||
|
name: e.name,
|
||||||
|
short_names: [e.name],
|
||||||
|
text: e.name,
|
||||||
|
emoticons: [],
|
||||||
|
keywords: [e.name],
|
||||||
|
imageUrl: e.image
|
||||||
|
}))
|
||||||
|
emojiData.value = new EmojiIndex(emojiDefault, { custom: customEmojis })
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(inReplyTo, current => {
|
||||||
|
if (current) {
|
||||||
|
form.status = `@${current.acct} `
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const post = async () => {
|
||||||
|
if (!client.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let options = {
|
||||||
|
visibility: visibility.value
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
if (attachments.value.length > 0) {
|
||||||
|
options = Object.assign(options, {
|
||||||
|
media_ids: attachments.value.map(m => m.id)
|
||||||
|
})
|
||||||
|
if (nsfw.value) {
|
||||||
|
options = Object.assign(options, {
|
||||||
|
sensitive: nsfw.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (form.spoiler.length > 0) {
|
||||||
|
options = Object.assign(options, {
|
||||||
|
spoiler_text: form.spoiler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (inReplyTo.value) {
|
||||||
|
options = Object.assign(options, {
|
||||||
|
in_reply_to_id: inReplyTo.value?.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poll.options.length > 0) {
|
||||||
|
options = Object.assign(options, {
|
||||||
|
poll: poll
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.value.postStatus(form.status, options)
|
||||||
|
clear()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
form.status = ''
|
||||||
|
form.spoiler = ''
|
||||||
|
attachments.value = []
|
||||||
|
cw.value = false
|
||||||
|
emojiVisible.value = false
|
||||||
|
store.commit(`${space}/${MUTATION_TYPES.CLEAR_REPLY_TO_ID}`)
|
||||||
|
poll.options = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectImage = () => {
|
||||||
|
imageRef?.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChangeImage = async (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const file = target.files?.item(0)
|
||||||
|
if (file === null || file === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await uploadImage(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadImage = async (file: File) => {
|
||||||
|
if (!client.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const res = await client.value.uploadMedia(file)
|
||||||
|
attachments.value = [...attachments.value, res.data]
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAttachment = async (attachment: Entity.Attachment | Entity.AsyncAttachment) => {
|
||||||
|
attachments.value = attachments.value.filter(e => e.id !== attachment.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectEmoji = emoji => {
|
||||||
|
if (!statusRef.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const current = statusRef.value?.textarea.selectionStart
|
||||||
|
if (emoji.native) {
|
||||||
|
form.status = form.status.slice(0, current) + emoji.native + form.status.slice(current)
|
||||||
|
} else {
|
||||||
|
form.status = form.status.slice(0, current) + emoji.name + form.status.slice(current)
|
||||||
|
}
|
||||||
|
emojiVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeVisibility = (value: 'public' | 'unlisted' | 'private' | 'direct') => {
|
||||||
|
visibility.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePoll = () => {
|
||||||
|
if (poll.options.length > 0) {
|
||||||
|
poll.options = []
|
||||||
|
} else {
|
||||||
|
poll.options = ['', '']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPollOption = () => {
|
||||||
|
poll.options = [...poll.options, '']
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePollOption = (index: number) => {
|
||||||
|
poll.options = poll.options.filter((_, i) => i !== index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
post,
|
||||||
|
imageRef,
|
||||||
|
selectImage,
|
||||||
|
onChangeImage,
|
||||||
|
loading,
|
||||||
|
attachments,
|
||||||
|
removeAttachment,
|
||||||
|
emojiData,
|
||||||
|
selectEmoji,
|
||||||
|
statusRef,
|
||||||
|
emojiVisible,
|
||||||
|
cw,
|
||||||
|
visibilityList,
|
||||||
|
visibilityIcon,
|
||||||
|
changeVisibility,
|
||||||
|
nsfw,
|
||||||
|
poll,
|
||||||
|
togglePoll,
|
||||||
|
expiresList,
|
||||||
|
addPollOption,
|
||||||
|
removePollOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.compose {
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.compose-form {
|
||||||
|
height: calc(100% - 24px);
|
||||||
|
|
||||||
|
.spoiler {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-form :deep(.el-textarea__inner) {
|
||||||
|
color: var(--theme-primary-color);
|
||||||
|
background-color: var(--theme-background-color);
|
||||||
|
box-shadow: 0 0 0 1px var(--theme-border-color, var(--theme-border-color)) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-form :deep(.el-input__wrapper) {
|
||||||
|
background-color: var(--theme-background-color);
|
||||||
|
box-shadow: 0 0 0 1px var(--theme-border-color, var(--theme-border-color)) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-form :deep(.el-input__inner) {
|
||||||
|
color: var(--theme-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-width: 10%;
|
||||||
|
height: 80px;
|
||||||
|
margin: 4px;
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
.fa-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: auto;
|
||||||
|
height: 1em;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll {
|
||||||
|
.options-list {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 16px;
|
||||||
|
|
||||||
|
.poll-option {
|
||||||
|
line-height: 38px;
|
||||||
|
|
||||||
|
.remove-poll {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-poll {
|
||||||
|
margin: 0 4px 0 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll :deep(.el-input__inner) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
.tool-buttons {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
span {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: var(--theme-secondary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.new-toot-emoji-picker {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
|
||||||
|
.el-popper__arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -27,12 +27,6 @@
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</DynamicScroller>
|
</DynamicScroller>
|
||||||
|
|
||||||
<div :class="openSideBar ? 'upper-with-side-bar' : 'upper'" v-show="!heading">
|
|
||||||
<el-button type="primary" @click="upper" circle>
|
|
||||||
<font-awesome-icon icon="angle-up" class="upper-icon" />
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -204,10 +198,6 @@ export default defineComponent({
|
||||||
}, 500)
|
}, 500)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const upper = () => {
|
|
||||||
scroller.value.scrollToItem(0)
|
|
||||||
focusedId.value = null
|
|
||||||
}
|
|
||||||
const focusNext = () => {
|
const focusNext = () => {
|
||||||
if (currentFocusedIndex.value === -1) {
|
if (currentFocusedIndex.value === -1) {
|
||||||
focusedId.value = timeline.value[0].uri + timeline.value[0].id
|
focusedId.value = timeline.value[0].uri + timeline.value[0].id
|
||||||
|
@ -245,7 +235,6 @@ export default defineComponent({
|
||||||
focusToot,
|
focusToot,
|
||||||
openSideBar,
|
openSideBar,
|
||||||
heading,
|
heading,
|
||||||
upper,
|
|
||||||
account
|
account
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -269,39 +258,6 @@ export default defineComponent({
|
||||||
.loading-card:empty {
|
.loading-card:empty {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upper {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
right: 20px;
|
|
||||||
transition: all 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upper-with-side-bar {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
right: calc(20px + var(--current-sidebar-width));
|
|
||||||
transition: all 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upper-icon {
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unread {
|
|
||||||
position: fixed;
|
|
||||||
right: 24px;
|
|
||||||
top: 52px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 0 0 2px 2px;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -255,9 +255,9 @@ import { usernameWithStyle, accountNameWithStyle } from '@/utils/username'
|
||||||
import { parseDatetime } from '@/utils/datetime'
|
import { parseDatetime } from '@/utils/datetime'
|
||||||
import { MUTATION_TYPES as SIDEBAR_MUTATION, ACTION_TYPES as SIDEBAR_ACTION } from '@/store/TimelineSpace/Contents/SideBar'
|
import { MUTATION_TYPES as SIDEBAR_MUTATION, ACTION_TYPES as SIDEBAR_ACTION } from '@/store/TimelineSpace/Contents/SideBar'
|
||||||
import { ACTION_TYPES as PROFILE_ACTION } from '@/store/TimelineSpace/Contents/SideBar/AccountProfile'
|
import { ACTION_TYPES as PROFILE_ACTION } from '@/store/TimelineSpace/Contents/SideBar/AccountProfile'
|
||||||
import { ACTION_TYPES as NEW_ACTION } from '@/store/TimelineSpace/Modals/NewToot'
|
|
||||||
import { ACTION_TYPES as DETAIL_ACTION } from '@/store/TimelineSpace/Contents/SideBar/TootDetail'
|
import { ACTION_TYPES as DETAIL_ACTION } from '@/store/TimelineSpace/Contents/SideBar/TootDetail'
|
||||||
import { ACTION_TYPES as REPORT_ACTION } from '@/store/TimelineSpace/Modals/Report'
|
import { ACTION_TYPES as REPORT_ACTION } from '@/store/TimelineSpace/Modals/Report'
|
||||||
|
import { MUTATION_TYPES as COMPOSE_MUTATION } from '@/store/TimelineSpace/Compose'
|
||||||
import { ACTION_TYPES as MUTE_ACTION } from '@/store/TimelineSpace/Modals/MuteConfirm'
|
import { ACTION_TYPES as MUTE_ACTION } from '@/store/TimelineSpace/Modals/MuteConfirm'
|
||||||
import { ACTION_TYPES as VIEWER_ACTION } from '@/store/TimelineSpace/Modals/ImageViewer'
|
import { ACTION_TYPES as VIEWER_ACTION } from '@/store/TimelineSpace/Modals/ImageViewer'
|
||||||
import { ACTION_TYPES } from '@/store/organisms/Toot'
|
import { ACTION_TYPES } from '@/store/organisms/Toot'
|
||||||
|
@ -477,7 +477,10 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const openReply = () => {
|
const openReply = () => {
|
||||||
store.dispatch(`TimelineSpace/Modals/NewToot/${NEW_ACTION.OPEN_REPLY}`, originalMessage.value)
|
store.commit(`TimelineSpace/Compose/${COMPOSE_MUTATION.SET_REPLY_TO_ID}`, {
|
||||||
|
acct: originalMessage.value.account.acct,
|
||||||
|
id: originalMessage.value.id
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const openDetail = (message: Entity.Status) => {
|
const openDetail = (message: Entity.Status) => {
|
||||||
store.dispatch(`TimelineSpace/Contents/SideBar/${SIDEBAR_ACTION.OPEN_TOOT_COMPONENT}`)
|
store.dispatch(`TimelineSpace/Contents/SideBar/${SIDEBAR_ACTION.OPEN_TOOT_COMPONENT}`)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { MyWindow } from '~/src/types/global'
|
||||||
import { LocalServer } from '~/src/types/localServer'
|
import { LocalServer } from '~/src/types/localServer'
|
||||||
import { Setting } from '~/src/types/setting'
|
import { Setting } from '~/src/types/setting'
|
||||||
import { DefaultSetting } from '~/src/constants/initializer/setting'
|
import { DefaultSetting } from '~/src/constants/initializer/setting'
|
||||||
|
import Compose, { ComposeState } from './TimelineSpace/Compose'
|
||||||
|
|
||||||
const win = (window as any) as MyWindow
|
const win = (window as any) as MyWindow
|
||||||
|
|
||||||
|
@ -201,6 +202,7 @@ type TimelineSpaceModule = {
|
||||||
HeaderMenu: HeaderMenuState
|
HeaderMenu: HeaderMenuState
|
||||||
Modals: ModalsModuleState
|
Modals: ModalsModuleState
|
||||||
Contents: ContentsModuleState
|
Contents: ContentsModuleState
|
||||||
|
Compose: ComposeState
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TimelineSpaceModuleState = TimelineSpaceModule & TimelineSpaceState
|
export type TimelineSpaceModuleState = TimelineSpaceModule & TimelineSpaceState
|
||||||
|
@ -211,7 +213,8 @@ const TimelineSpace: Module<TimelineSpaceState, RootState> = {
|
||||||
SideMenu,
|
SideMenu,
|
||||||
HeaderMenu,
|
HeaderMenu,
|
||||||
Modals,
|
Modals,
|
||||||
Contents
|
Contents,
|
||||||
|
Compose
|
||||||
},
|
},
|
||||||
state: state,
|
state: state,
|
||||||
mutations: mutations,
|
mutations: mutations,
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Module, MutationTree } from 'vuex'
|
||||||
|
import { RootState } from '@/store'
|
||||||
|
|
||||||
|
export type ReplyTo = {
|
||||||
|
acct: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComposeState = {
|
||||||
|
inReplyTo: ReplyTo | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = (): ComposeState => ({
|
||||||
|
inReplyTo: null
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MUTATION_TYPES = {
|
||||||
|
SET_REPLY_TO_ID: 'setReplyToId',
|
||||||
|
CLEAR_REPLY_TO_ID: 'clearReplyToId'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations: MutationTree<ComposeState> = {
|
||||||
|
[MUTATION_TYPES.SET_REPLY_TO_ID]: (state, inReplyTo: ReplyTo) => {
|
||||||
|
state.inReplyTo = inReplyTo
|
||||||
|
},
|
||||||
|
[MUTATION_TYPES.CLEAR_REPLY_TO_ID]: state => {
|
||||||
|
state.inReplyTo = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Compose: Module<ComposeState, RootState> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: state,
|
||||||
|
mutations: mutations
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Compose
|
Loading…
Reference in New Issue