Merge pull request #902 from h3poteto/iss-884

closes #884 Add request loading circle
This commit is contained in:
AkiraFukushima 2019-05-07 23:27:36 +09:00 committed by GitHub
commit 2e1efb8926
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 366 additions and 202 deletions

View File

@ -14,7 +14,8 @@ const list: List = {
const state = (): HeaderMenuState => {
return {
title: 'Home',
reload: false
reload: false,
loading: false
}
}

View File

@ -6,7 +6,8 @@ describe('TimelineSpace/HeaderMenu', () => {
beforeEach(() => {
state = {
title: 'Home',
reload: false
reload: false,
loading: false
}
})
describe('changeReload', () => {

View File

@ -0,0 +1,57 @@
<svg class="lds-spinner" width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="background: rgba(0, 0, 0, 0) none repeat scroll 0% 0%;"><g transform="rotate(0 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9285714285714286s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(25.714285714285715 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8571428571428571s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(51.42857142857143 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.7857142857142857s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(77.14285714285714 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.7142857142857143s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(102.85714285714286 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6428571428571429s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(128.57142857142858 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5714285714285714s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(154.28571428571428 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(180 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.42857142857142855s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(205.71428571428572 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.35714285714285715s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(231.42857142857142 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.2857142857142857s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(257.14285714285717 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.21428571428571427s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(282.85714285714283 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.14285714285714285s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(308.57142857142856 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.07142857142857142s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(334.2857142857143 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
</rect>
</g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,59 +1,66 @@
<template>
<nav id="header_menu" :aria-label="title">
<div class="channel">
<h1>{{ title }}</h1>
</div>
<div class="tools">
<el-button v-if="!pleroma" type="text" class="action" @click="switchStreaming" :title="$t('header_menu.switch_streaming')">
<svg :class="useWebsocket ? 'websocket' : 'not-websocket'" width="25" height="18" viewBox="0 0 256 193" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M192.44 144.645h31.78V68.339l-35.805-35.804-22.472 22.472 26.497 26.497v63.14zm31.864 15.931H113.452L86.954 134.08l11.237-11.236 21.885 21.885h45.028l-44.357-44.441 11.32-11.32 44.357 44.358V88.296l-21.801-21.801 11.152-11.153L110.685 0H0l31.696 31.696v.084H97.436l23.227 23.227-33.96 33.96L63.476 65.74V47.712h-31.78v31.193l55.007 55.007L64.314 156.3l35.805 35.805H256l-31.696-31.529z" /></svg>
</el-button>
<el-button type="text" class="action" @click="openNewTootModal" :title="$t('header_menu.new_toot')">
<icon name="regular/edit" scale="1.1"></icon>
</el-button>
<el-button v-show="reloadable()" type="text" class="action" @click="reload" :title="$t('header_menu.reload')">
<icon name="sync-alt"></icon>
</el-button>
<el-popover
placement="left-start"
width="320"
popper-class="theme-popover"
trigger="click"
v-model="filterVisible">
<div>
<el-form role="form" label-position="left" label-width="125px" size="medium">
<el-form-item for="filter" :label="$t('header_menu.filter.title')">
<div class="el-input">
<input
id="filter"
class="el-input__inner"
v-model="filter"
:placeholder="$t('header_menu.filter.placeholder')"
v-shortkey.avoid
:aria-label="$t('header_menu.filter.placeholder')"
:title="$t('header_menu.filter.placeholder')"
>
</div>
</el-form-item>
<el-form-item for="show-reblogs" :label="$t('header_menu.filter.show_reblogs')" v-if="extrasFilterable()">
<el-checkbox id="show-reblogs" v-model="showReblogs"></el-checkbox>
</el-form-item>
<el-form-item for="show-replies" :label="$t('header_menu.filter.show_replies')" v-if="extrasFilterable()">
<el-checkbox id="show-replies" v-model="showReplies"></el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="applyFilter(filter)">{{ $t('header_menu.filter.apply') }}</el-button>
</el-form-item>
</el-form>
</div>
<el-button v-show="filterable()" slot="reference" type="text" class="action" :title="$t('header_menu.filter.title')">
<icon name="sliders-h"></icon>
<nav id="header_menu" :aria-label="title">
<div class="channel">
<h1>{{ title }}</h1>
</div>
<div class="tools">
<img src="../../assets/images/loading-spinner-wide.svg" v-show="loading" class="header-loading" />
<el-button v-if="!pleroma" type="text" class="action" @click="switchStreaming" :title="$t('header_menu.switch_streaming')">
<svg
:class="useWebsocket ? 'websocket' : 'not-websocket'"
width="25"
height="18"
viewBox="0 0 256 193"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
>
<path
d="M192.44 144.645h31.78V68.339l-35.805-35.804-22.472 22.472 26.497 26.497v63.14zm31.864 15.931H113.452L86.954 134.08l11.237-11.236 21.885 21.885h45.028l-44.357-44.441 11.32-11.32 44.357 44.358V88.296l-21.801-21.801 11.152-11.153L110.685 0H0l31.696 31.696v.084H97.436l23.227 23.227-33.96 33.96L63.476 65.74V47.712h-31.78v31.193l55.007 55.007L64.314 156.3l35.805 35.805H256l-31.696-31.529z"
/>
</svg>
</el-button>
</el-popover>
<el-button type="text" class="action" @click="settings" :title="$t('header_menu.settings')">
<icon name="cog" scale="1.1"></icon>
</el-button>
</div>
</nav>
<el-button type="text" class="action" @click="openNewTootModal" :title="$t('header_menu.new_toot')">
<icon name="regular/edit" scale="1.1"></icon>
</el-button>
<el-button v-show="reloadable()" type="text" class="action" @click="reload" :title="$t('header_menu.reload')">
<icon name="sync-alt"></icon>
</el-button>
<el-popover placement="left-start" width="320" popper-class="theme-popover" trigger="click" v-model="filterVisible">
<div>
<el-form role="form" label-position="left" label-width="125px" size="medium">
<el-form-item for="filter" :label="$t('header_menu.filter.title')">
<div class="el-input">
<input
id="filter"
class="el-input__inner"
v-model="filter"
:placeholder="$t('header_menu.filter.placeholder')"
v-shortkey.avoid
:aria-label="$t('header_menu.filter.placeholder')"
:title="$t('header_menu.filter.placeholder')"
/>
</div>
</el-form-item>
<el-form-item for="show-reblogs" :label="$t('header_menu.filter.show_reblogs')" v-if="extrasFilterable()">
<el-checkbox id="show-reblogs" v-model="showReblogs"></el-checkbox>
</el-form-item>
<el-form-item for="show-replies" :label="$t('header_menu.filter.show_replies')" v-if="extrasFilterable()">
<el-checkbox id="show-replies" v-model="showReplies"></el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="applyFilter(filter)">{{ $t('header_menu.filter.apply') }}</el-button>
</el-form-item>
</el-form>
</div>
<el-button v-show="filterable()" slot="reference" type="text" class="action" :title="$t('header_menu.filter.title')">
<icon name="sliders-h"></icon>
</el-button>
</el-popover>
<el-button type="text" class="action" @click="settings" :title="$t('header_menu.settings')">
<icon name="cog" scale="1.1"></icon>
</el-button>
</div>
</nav>
</template>
<script>
@ -61,7 +68,7 @@ import { mapState } from 'vuex'
export default {
name: 'header-menu',
data () {
data() {
return {
filter: '',
filterVisible: false,
@ -71,28 +78,30 @@ export default {
},
computed: {
...mapState('TimelineSpace/HeaderMenu', {
title: state => state.title
title: state => state.title,
loading: state => state.loading
}),
...mapState('TimelineSpace', {
useWebsocket: state => state.useWebsocket,
pleroma: state => state.pleroma
})
},
created () {
created() {
this.channelName()
this.loadFilter()
this.$store.dispatch('TimelineSpace/HeaderMenu/setupLoading')
},
watch: {
'$route': function () {
$route: function() {
this.channelName()
this.loadFilter()
}
},
methods: {
id () {
id() {
return this.$route.params.id
},
channelName () {
channelName() {
switch (this.$route.name) {
case 'home':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.home'))
@ -139,15 +148,15 @@ export default {
break
}
},
switchStreaming () {
switchStreaming() {
this.$store.dispatch('TimelineSpace/stopStreamings')
this.$store.commit('TimelineSpace/changeUseWebsocket', !this.useWebsocket)
this.$store.dispatch('TimelineSpace/startStreamings')
},
openNewTootModal () {
openNewTootModal() {
this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal')
},
reload () {
reload() {
switch (this.$route.name) {
case 'home':
case 'notifications':
@ -164,7 +173,7 @@ export default {
console.log('Not implemented')
}
},
reloadable () {
reloadable() {
switch (this.$route.name) {
case 'home':
case 'notifications':
@ -180,7 +189,7 @@ export default {
return false
}
},
loadFilter () {
loadFilter() {
switch (this.$route.name) {
case 'home':
this.filter = this.$store.state.TimelineSpace.Contents.Home.filter
@ -215,7 +224,7 @@ export default {
console.log('Not implemented')
}
},
applyFilter (filter) {
applyFilter(filter) {
switch (this.$route.name) {
case 'home':
this.$store.commit('TimelineSpace/Contents/Home/changeFilter', filter)
@ -251,7 +260,7 @@ export default {
}
this.filterVisible = false
},
filterable () {
filterable() {
switch (this.$route.name) {
case 'home':
case 'notifications':
@ -267,7 +276,7 @@ export default {
return false
}
},
extrasFilterable () {
extrasFilterable() {
switch (this.$route.name) {
case 'home':
return true
@ -275,7 +284,7 @@ export default {
return false
}
},
settings () {
settings() {
const url = `/${this.id()}/settings`
this.$router.push(url)
}
@ -303,6 +312,13 @@ export default {
.tools {
font-size: 18px;
display: flex;
justify-content: flex-end;
align-items: center;
.header-loading {
width: 18px;
}
.action {
color: var(--theme-secondary-color);

View File

@ -4,20 +4,15 @@
:visible.sync="newTootModal"
:before-close="closeConfirm"
width="400px"
class="new-toot-modal">
class="new-toot-modal"
>
<el-form v-on:submit.prevent="toot" role="form">
<div class="spoiler" v-show="showContentWarning">
<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"
@paste="onPaste"
@toot="toot"
/>
<Status v-model="status" :opened="newTootModal" :fixCursorPos="hashtagInserting" @paste="onPaste" @toot="toot" />
</el-form>
<div class="preview">
<div class="image-wrapper" v-for="media in attachedMedias" v-bind:key="media.id">
@ -28,11 +23,12 @@
class="image-description"
:placeholder="$t('modals.new_toot.description')"
v-model="mediaDescriptions[media.id]"
v-shortkey="{left: ['arrowleft'], right: ['arrowright']}"
v-shortkey="{ left: ['arrowleft'], right: ['arrowright'] }"
@shortkey="handleDescriptionKey"
role="textbox"
contenteditable="true"
aria-multiline="true">
aria-multiline="true"
>
</textarea>
</div>
</div>
@ -41,7 +37,7 @@
<el-button size="small" type="text" @click="selectImage" :title="$t('modals.new_toot.add_image')">
<icon name="camera"></icon>
</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="image" @change="onChangeImage" :key="attachedMediaId" />
</div>
<div class="privacy">
<el-dropdown trigger="click" @command="changeVisibility">
@ -69,24 +65,50 @@
</el-dropdown>
</div>
<div class="sensitive" v-show="attachedMedias.length > 0">
<el-button size="small" type="text" @click="changeSensitive" :title="$t('modals.new_toot.change_sensitive')" :aria-pressed="sensitive">
<el-button
size="small"
type="text"
@click="changeSensitive"
:title="$t('modals.new_toot.change_sensitive')"
:aria-pressed="sensitive"
>
<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.add_cw')" :class="showContentWarning? '' : 'clickable'" :aria-pressed="showContentWarning">
<el-button
size="small"
type="text"
@click="showContentWarning = !showContentWarning"
:title="$t('modals.new_toot.add_cw')"
:class="showContentWarning ? '' : 'clickable'"
:aria-pressed="showContentWarning"
>
<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.pined_hashtag')" :class="pinedHashtag? '' : 'clickable'" :aria-pressed="pinedHashtag">
<el-button
size="small"
type="text"
@click="pinedHashtag = !pinedHashtag"
:title="$t('modals.new_toot.pined_hashtag')"
:class="pinedHashtag ? '' : 'clickable'"
:aria-pressed="pinedHashtag"
>
<icon name="hashtag"></icon>
</el-button>
</div>
<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 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>
<div class="clearfix"></div>
</div>
</el-dialog>
@ -103,7 +125,7 @@ export default {
components: {
Status
},
data () {
data() {
return {
status: '',
mediaDescriptions: {},
@ -114,7 +136,7 @@ export default {
},
computed: {
...mapState('TimelineSpace/Modals/NewToot', {
replyToId: (state) => {
replyToId: state => {
if (state.replyToMessage !== null) {
return state.replyToMessage.id
} else {
@ -128,7 +150,7 @@ export default {
sensitive: state => state.sensitive,
initialStatus: state => state.initialStatus,
initialSpoiler: state => state.initialSpoiler,
visibilityIcon: (state) => {
visibilityIcon: state => {
switch (state.visibility) {
case Visibility.Public.value:
return 'globe'
@ -141,19 +163,18 @@ export default {
default:
return 'globe'
}
}
},
loading: state => state.loading
}),
...mapState('TimelineSpace', {
tootMax: state => state.tootMax
}),
...mapGetters('TimelineSpace/Modals/NewToot', [
'hashtagInserting'
]),
...mapGetters('TimelineSpace/Modals/NewToot', ['hashtagInserting']),
newTootModal: {
get () {
get() {
return this.$store.state.TimelineSpace.Modals.NewToot.modalOpen
},
set (value) {
set(value) {
if (value) {
this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal')
} else {
@ -162,16 +183,19 @@ export default {
}
},
pinedHashtag: {
get () {
get() {
return this.$store.state.TimelineSpace.Modals.NewToot.pinedHashtag
},
set (value) {
set(value) {
this.$store.commit('TimelineSpace/Modals/NewToot/changePinedHashtag', value)
}
}
},
created() {
this.$store.dispatch('TimelineSpace/Modals/NewToot/setupLoading')
},
watch: {
newTootModal: function (newState, oldState) {
newTootModal: function(newState, oldState) {
if (!oldState && newState) {
this.showContentWarning = this.initialSpoiler
this.status = this.initialStatus
@ -180,12 +204,12 @@ export default {
}
},
methods: {
close () {
close() {
this.filteredAccount = []
this.$store.dispatch('TimelineSpace/Modals/NewToot/resetMediaId')
this.$store.dispatch('TimelineSpace/Modals/NewToot/closeModal')
},
async toot () {
async toot() {
if (!this.newTootModal) {
return
}
@ -195,7 +219,7 @@ export default {
type: 'error'
})
}
const visibilityKey = Object.keys(Visibility).find((key) => {
const visibilityKey = Object.keys(Visibility).find(key => {
return Visibility[key].value === this.visibility
})
let form = {
@ -217,15 +241,18 @@ export default {
})
}
form = Object.assign(form, {
media_ids: this.attachedMedias.map((m) => { return m.id })
media_ids: this.attachedMedias.map(m => {
return m.id
})
})
}
const status = await this.$store.dispatch('TimelineSpace/Modals/NewToot/updateMedia', this.mediaDescriptions)
const status = await this.$store
.dispatch('TimelineSpace/Modals/NewToot/updateMedia', this.mediaDescriptions)
.then(() => {
return this.$store.dispatch('TimelineSpace/Modals/NewToot/postToot', form)
})
.catch((e) => {
.catch(e => {
console.error(e)
this.$message({
message: this.$t('message.toot_error'),
@ -235,10 +262,10 @@ export default {
this.$store.dispatch('TimelineSpace/Modals/NewToot/updateHashtags', status.tags)
this.close()
},
selectImage () {
selectImage() {
this.$refs.image.click()
},
onChangeImage (e) {
onChangeImage(e) {
if (e.target.files.item(0) === null || e.target.files.item(0) === undefined) {
return
}
@ -252,7 +279,7 @@ export default {
}
this.updateImage(file)
},
onPaste (e) {
onPaste(e) {
const mimeTypes = clipboard.availableFormats().filter(type => type.startsWith('image'))
if (mimeTypes.length === 0) {
return
@ -268,43 +295,40 @@ export default {
const file = new File([data], 'clipboard.picture', { type: mimeTypes[0] })
this.updateImage(file)
},
updateImage (file) {
updateImage(file) {
this.$store.dispatch('TimelineSpace/Modals/NewToot/incrementMediaId')
this.$store.dispatch('TimelineSpace/Modals/NewToot/uploadImage', file)
.catch(() => {
this.$message({
message: this.$t('message.attach_error'),
type: 'error'
})
this.$store.dispatch('TimelineSpace/Modals/NewToot/uploadImage', file).catch(() => {
this.$message({
message: this.$t('message.attach_error'),
type: 'error'
})
})
},
removeAttachment (media) {
removeAttachment(media) {
this.$store.commit('TimelineSpace/Modals/NewToot/removeMedia', media)
delete this.mediaDescriptions[media.id]
},
changeVisibility (level) {
changeVisibility(level) {
this.$store.commit('TimelineSpace/Modals/NewToot/changeVisibilityValue', level)
},
changeSensitive () {
changeSensitive() {
this.$store.commit('TimelineSpace/Modals/NewToot/changeSensitive', !this.sensitive)
},
closeConfirm (done) {
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')
})
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) {
handleDescriptionKey(event) {
const current = event.target.selectionStart
switch (event.srcKey) {
case 'left':
@ -451,9 +475,20 @@ export default {
color: #909399;
}
.text-count {
padding-right: 10px;
color: #909399;
.info {
display: flex;
justify-content: flex-end;
align-items: center;
.loading {
width: 18px;
margin-right: 4px;
}
.text-count {
padding-right: 10px;
color: #909399;
}
}
.toot-action {

View File

@ -1,20 +1,24 @@
import Mastodon, { List, Response } from 'megalodon'
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '@/store'
import AxiosLoading from '@/utils/axiosLoading'
export interface HeaderMenuState {
title: string,
title: string
reload: boolean
loading: boolean
}
const state = (): HeaderMenuState => ({
title: 'Home',
reload: false
reload: false,
loading: false
})
export const MUTATION_TYPES = {
UPDATE_TITLE: 'updateTitle',
CHANGE_RELOAD: 'changeReload'
CHANGE_RELOAD: 'changeReload',
CHANGE_LOADING: 'changeLoading'
}
const mutations: MutationTree<HeaderMenuState> = {
@ -23,18 +27,27 @@ const mutations: MutationTree<HeaderMenuState> = {
},
[MUTATION_TYPES.CHANGE_RELOAD]: (state, value: boolean) => {
state.reload = value
},
[MUTATION_TYPES.CHANGE_LOADING]: (state, value: boolean) => {
state.loading = value
}
}
const actions: ActionTree<HeaderMenuState, RootState> = {
fetchList: async ({ commit, rootState }, listID: number): Promise<List> => {
const client = new Mastodon(
rootState.TimelineSpace.account.accessToken!,
rootState.TimelineSpace.account.baseURL + '/api/v1'
)
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<List> = await client.get<List>(`/lists/${listID}`)
commit(MUTATION_TYPES.UPDATE_TITLE, `#${res.data.title}`)
return res.data
},
setupLoading: ({ commit }) => {
const axiosLoading = new AxiosLoading()
axiosLoading.on('start', (_: number) => {
commit(MUTATION_TYPES.CHANGE_LOADING, true)
})
axiosLoading.on('done', () => {
commit(MUTATION_TYPES.CHANGE_LOADING, false)
})
}
}

View File

@ -4,19 +4,21 @@ import Visibility, { VisibilityType } from '~/src/constants/visibility'
import TootStatus, { StatusState } from './NewToot/Status'
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
import { RootState } from '@/store'
import AxiosLoading from '@/utils/axiosLoading'
export interface NewTootState {
modalOpen: boolean,
initialStatus: string,
initialSpoiler: string,
replyToMessage: Status | null,
blockSubmit: boolean,
attachedMedias: Array<Attachment>,
visibility: number,
sensitive: boolean,
attachedMediaId: number,
pinedHashtag: boolean,
modalOpen: boolean
initialStatus: string
initialSpoiler: string
replyToMessage: Status | null
blockSubmit: boolean
attachedMedias: Array<Attachment>
visibility: number
sensitive: boolean
attachedMediaId: number
pinedHashtag: boolean
hashtags: Array<Tag>
loading: boolean
}
export interface NewTootModuleState extends NewTootState {
@ -34,7 +36,8 @@ const state = (): NewTootState => ({
sensitive: false,
attachedMediaId: 0,
pinedHashtag: false,
hashtags: []
hashtags: [],
loading: false
})
export const MUTATION_TYPES = {
@ -50,7 +53,8 @@ export const MUTATION_TYPES = {
CHANGE_SENSITIVE: 'changeSensitive',
UPDATE_MEDIA_ID: 'updateMediaId',
CHANGE_PINED_HASHTAG: 'changePinedHashtag',
UPDATE_HASHTAGS: 'updateHashtags'
UPDATE_HASHTAGS: 'updateHashtags',
CHANGE_LOADING: 'changeLoading'
}
const mutations: MutationTree<NewTootState> = {
@ -72,7 +76,7 @@ const mutations: MutationTree<NewTootState> = {
[MUTATION_TYPES.APPEND_ATTACHED_MEDIAS]: (state, media: Attachment) => {
state.attachedMedias = state.attachedMedias.concat([media])
},
[MUTATION_TYPES.CLEAR_ATTACHED_MEDIAS]: (state) => {
[MUTATION_TYPES.CLEAR_ATTACHED_MEDIAS]: state => {
state.attachedMedias = []
},
[MUTATION_TYPES.REMOVE_MEDIA]: (state, media: Attachment) => {
@ -98,26 +102,34 @@ const mutations: MutationTree<NewTootState> = {
},
[MUTATION_TYPES.UPDATE_HASHTAGS]: (state, tags: Array<Tag>) => {
state.hashtags = tags
},
[MUTATION_TYPES.CHANGE_LOADING]: (state, value: boolean) => {
state.loading = value
}
}
const actions: ActionTree<NewTootState, RootState> = {
setupLoading: ({ commit }) => {
const axiosLoading = new AxiosLoading()
axiosLoading.on('start', (_: number) => {
commit(MUTATION_TYPES.CHANGE_LOADING, true)
})
axiosLoading.on('done', () => {
commit(MUTATION_TYPES.CHANGE_LOADING, false)
})
},
updateMedia: async ({ rootState }, media: Attachment) => {
if (rootState.TimelineSpace.account.accessToken === undefined || rootState.TimelineSpace.account.accessToken === null) {
throw new AuthenticationError()
}
const client = new Mastodon(
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.account.baseURL + '/api/v1'
)
const client = new Mastodon(rootState.TimelineSpace.account.accessToken, rootState.TimelineSpace.account.baseURL + '/api/v1')
const attachments = Object.keys(media).map(async id => {
return client.put<Attachment>(`/media/${id}`, { description: media[id] })
})
return Promise.all(attachments)
.catch(err => {
console.error(err)
throw err
})
return Promise.all(attachments).catch(err => {
console.error(err)
throw err
})
},
postToot: async ({ state, commit, rootState }, form) => {
if (rootState.TimelineSpace.account.accessToken === undefined || rootState.TimelineSpace.account.accessToken === null) {
@ -127,11 +139,9 @@ const actions: ActionTree<NewTootState, RootState> = {
return
}
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, true)
const client = new Mastodon(
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.account.baseURL + '/api/v1'
)
return client.post<Status>('/statuses', form)
const client = new Mastodon(rootState.TimelineSpace.account.accessToken, rootState.TimelineSpace.account.baseURL + '/api/v1')
return client
.post<Status>('/statuses', form)
.then((res: Response<Status>) => {
ipcRenderer.send('toot-action-sound')
return res.data
@ -142,9 +152,10 @@ const actions: ActionTree<NewTootState, RootState> = {
},
openReply: ({ commit, rootState }, message: Status) => {
commit(MUTATION_TYPES.SET_REPLY_TO, message)
const mentionAccounts = [message.account.acct].concat(message.mentions.map(a => a.acct))
const mentionAccounts = [message.account.acct]
.concat(message.mentions.map(a => a.acct))
.filter((a, i, self) => self.indexOf(a) === i)
.filter((a) => a !== rootState.TimelineSpace.account.username)
.filter(a => a !== rootState.TimelineSpace.account.username)
commit(MUTATION_TYPES.UPDATE_INITIAL_STATUS, `${mentionAccounts.map(m => `@${m}`).join(' ')} `)
commit(MUTATION_TYPES.UPDATE_INITIAL_SPOILER, message.spoiler_text)
commit(MUTATION_TYPES.CHANGE_MODAL, true)
@ -179,13 +190,11 @@ const actions: ActionTree<NewTootState, RootState> = {
if (rootState.TimelineSpace.account.accessToken === undefined || rootState.TimelineSpace.account.accessToken === null) {
throw new AuthenticationError()
}
const client = new Mastodon(
rootState.TimelineSpace.account.accessToken,
rootState.TimelineSpace.account.baseURL + '/api/v1'
)
const client = new Mastodon(rootState.TimelineSpace.account.accessToken, rootState.TimelineSpace.account.baseURL + '/api/v1')
const formData = new FormData()
formData.append('file', image)
return client.post<Attachment>('/media', formData)
return client
.post<Attachment>('/media', formData)
.then(res => {
commit(MUTATION_TYPES.CHANGE_BLOCK_SUBMIT, false)
if (res.data.type === 'unknown') throw new UnknownTypeError()
@ -210,12 +219,9 @@ const actions: ActionTree<NewTootState, RootState> = {
}
},
fetchVisibility: async ({ commit, rootState }) => {
const client = new Mastodon(
rootState.TimelineSpace.account.accessToken!,
rootState.TimelineSpace.account.baseURL + '/api/v1'
)
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<Account> = await client.get<Account>('/accounts/verify_credentials')
const visibility: VisibilityType | undefined = (Object.values(Visibility) as Array<VisibilityType>).find((v) => {
const visibility: VisibilityType | undefined = (Object.values(Visibility) as Array<VisibilityType>).find(v => {
return v.key === res.data.source!.privacy
})
if (visibility === undefined) {
@ -227,7 +233,7 @@ const actions: ActionTree<NewTootState, RootState> = {
}
const getters: GetterTree<NewTootState, RootState> = {
hashtagInserting: (state) => {
hashtagInserting: state => {
return !state.replyToMessage && state.pinedHashtag
}
}

View File

@ -3,7 +3,7 @@ import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
import { RootState } from '@/store/index'
interface Suggest {
name: string,
name: string
image: string | null
}
@ -12,7 +12,7 @@ interface SuggestAccount extends Suggest {}
interface SuggestHashtag extends Suggest {}
export interface StatusState {
filteredAccounts: Array<SuggestAccount>,
filteredAccounts: Array<SuggestAccount>
filteredHashtags: Array<SuggestHashtag>
}
@ -25,7 +25,7 @@ export const MUTATION_TYPES = {
UPDATE_FILTERED_ACCOUNTS: 'updateFilteredAccounts',
CLEAR_FILTERED_ACCOUNTS: 'clearFilteredAccounts',
UPDATE_FILTERED_HASHTAGS: 'updateFilteredHashtags',
CLAER_FILTERED_HASHTAGS: 'clearFilteredHashtags'
CLEAR_FILTERED_HASHTAGS: 'clearFilteredHashtags'
}
const mutations: MutationTree<StatusState> = {
@ -35,7 +35,7 @@ const mutations: MutationTree<StatusState> = {
image: null
}))
},
[MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS]: (state) => {
[MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS]: state => {
state.filteredAccounts = []
},
[MUTATION_TYPES.UPDATE_FILTERED_HASHTAGS]: (state, tags: Array<Tag>) => {
@ -44,27 +44,21 @@ const mutations: MutationTree<StatusState> = {
image: null
}))
},
[MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS]: (state) => {
[MUTATION_TYPES.CLEAR_FILTERED_HASHTAGS]: state => {
state.filteredHashtags = []
}
}
const actions: ActionTree<StatusState, RootState> = {
searchAccount: async ({ commit, rootState }, word: string) => {
const client = new Mastodon(
rootState.TimelineSpace.account.accessToken!,
rootState.TimelineSpace.account.baseURL + '/api/v1'
)
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<Results> = await client.get<Results>('/search', { q: word, resolve: false })
commit(MUTATION_TYPES.UPDATE_FILTERED_ACCOUNTS, res.data.accounts)
if (res.data.accounts.length === 0) throw new Error('Empty')
return res.data.accounts
},
searchHashtag: async ({ commit, rootState }, word: string) => {
const client = new Mastodon(
rootState.TimelineSpace.account.accessToken!,
rootState.TimelineSpace.account.baseURL + '/api/v1'
)
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<Results> = await client.get<Results>('/search', { q: word })
commit(MUTATION_TYPES.UPDATE_FILTERED_HASHTAGS, res.data.hashtags)
if (res.data.hashtags.length === 0) throw new Error('Empty')
@ -74,18 +68,20 @@ const actions: ActionTree<StatusState, RootState> = {
const getters: GetterTree<StatusState, RootState> = {
pickerEmojis: (_state, _getters, rootState) => {
return rootState.TimelineSpace.emojis.filter((e, i, array) => {
return (array.findIndex(ar => e.name === ar.name) === i)
}).map(e => {
return {
name: e.name,
short_names: [e.name],
text: e.name,
emoticons: [],
keywords: [e.name],
imageUrl: e.image
}
})
return rootState.TimelineSpace.emojis
.filter((e, i, array) => {
return array.findIndex(ar => e.name === ar.name) === i
})
.map(e => {
return {
name: e.name,
short_names: [e.name],
text: e.name,
emoticons: [],
keywords: [e.name],
imageUrl: e.image
}
})
}
}

View File

@ -0,0 +1,39 @@
import axios, { AxiosResponse } from 'axios'
import { EventEmitter } from 'events'
class AxiosLoading extends EventEmitter {
public requestCounter: number
constructor() {
super()
this.requestCounter = 0
this.setupRequest()
this.setupResponse()
}
private setupRequest() {
axios.interceptors.request.use(config => {
this.requestCounter++
this.emit('start', this.requestCounter)
return config
})
}
private setupResponse() {
const response = (response: AxiosResponse) => {
if (--this.requestCounter === 0) {
this.emit('done', {})
}
return response
}
const error = (error: any) => {
if (--this.requestCounter === 0) {
this.emit('done', {})
}
return Promise.reject(error)
}
axios.interceptors.response.use(response, error)
}
}
export default AxiosLoading