mixit/src/services/mastodon/Client.vue

369 lines
11 KiB
Vue
Raw Normal View History

2019-05-01 15:07:08 +00:00
<template lang="pug">
2019-05-27 13:44:49 +00:00
.client
2019-05-29 07:02:43 +00:00
.statues(@scroll.passive="onScroll" v-show="!hasContext")
2019-05-01 15:07:08 +00:00
.header(v-if="hasNotifications") Accueil
2019-05-03 15:03:13 +00:00
success-loadable.list(:loadable="statues")
template(v-for="status in statues.get()")
2019-05-29 07:02:43 +00:00
status(v-if="showStatus(status)" :key="status.id" :status="status" :showMedia="options.showMedia" @mark="onStatusMark" @vote="onPollVote" @context="onShowContext")
2019-05-03 15:03:13 +00:00
.status(v-show="statues.loadingMore")
.service-loader
2019-05-29 07:02:43 +00:00
.context(v-if="hasContext")
.header(@click="closeContext")
| Context
span.date(@click.stop.prevent="closeContext")
.list
.ancestors
template(v-if="targetContext.isSuccess")
status(v-for="status in targetContext.get().ancestors" :key="status.id" :status="status" :showMedia="options.showMedia" @mark="onStatusMark" @vote="onPollVote" @context="onShowContext")
.service-loader(v-else)
status.selected(:status="targetStatus" :showMedia="options.showMedia" @mark="onStatusMark" @vote="onPollVote" @context="closeContext")
.descendants
template(v-if="targetContext.isSuccess")
status(v-for="status in targetContext.get().descendants" :key="status.id" :status="status" :showMedia="options.showMedia" @mark="onStatusMark" @vote="onPollVote" @context="onShowContext")
.service-loader(v-else)
2019-05-01 15:07:08 +00:00
.notifications(v-if="hasNotifications")
.header
| Notifications
span.date(@click.stop.prevent="onNotificationsClear")
.list
2019-05-03 15:03:13 +00:00
notification(v-for="notification in notifications.get()" :key="notification.id" :notification="notification"
2019-05-29 07:02:43 +00:00
:showMedia="options.showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark" @vote="onPollVote" @context="onShowContext")
2019-05-27 13:44:49 +00:00
.compose-toggle(@click="showCompose = !showCompose") 🖉
2019-05-28 09:51:06 +00:00
.emoji-list(v-if="options.showMedia" v-show="showCompose && showEmojis")
2019-05-28 07:35:50 +00:00
img.emoji(v-for="emoji in emojis.get()" @click="addEmoji(emoji.shortcode)" :src="emoji.static_url" :alt="emoji.shortcode" :title="emoji.shortcode")
2019-05-27 13:44:49 +00:00
.compose(v-show="showCompose")
textarea.content(v-model="compose.status" placeholder="message")
.options
2019-05-28 07:35:50 +00:00
.emojis
button(v-if="options.showMedia" @click="showEmojis = !showEmojis")
select(v-else @change="addEmoji($event.target.value)")
option(v-for="emoji in emojis.get()" :value="emoji.shortcode") {{ emoji.shortcode }}
2019-05-27 13:44:49 +00:00
.sens
label.note(for="sensitive") Sensitive:&nbsp;
input(id="sensitive" v-model="compose.sensitive" type="checkbox")
.cw
input(v-show="compose.sensitive" v-model="compose.spoiler_text" placeholder="content warning")
.visibility
select(v-model="compose.visibility")
option(value="public")
option(value="unlisted") 👁
option(value="private")
2019-05-27 13:44:49 +00:00
option(value="direct")
span.note {{ compose.visibility }}
button(@click="sendStatus") Toot
2019-05-01 15:07:08 +00:00
</template>
<script lang="ts">
import axios, { AxiosResponse } from 'axios'
import { Component, Mixins, Prop } from 'vue-property-decorator'
2019-05-01 15:07:08 +00:00
2019-05-02 16:28:00 +00:00
import ServiceClient from '@/components/ServiceClient'
import Lists from '@/helpers/lists/Lists'
2019-05-29 07:02:43 +00:00
import AxiosLoadable from '@/helpers/loadable/AxiosLoadable'
import AxiosLoadableMore from '@/helpers/loadable/AxiosLoadableMore'
2019-05-27 12:31:43 +00:00
import { AUTH, getHeaders, getRest } from './Mastodon.vue'
2019-05-03 15:03:13 +00:00
import Notification from './Notification.vue'
import Status from './Status.vue'
2019-05-29 07:02:43 +00:00
import { Context, Emoji, MarkStatus, Notification as INotification, Options, Poll, PollVote, Status as IStatus, StatusPost, TimelineType } from './Types'
const STREAMS = {
home: 'user',
local: 'public:local',
public: 'public'
}
2019-05-03 15:03:13 +00:00
@Component({ components: { Status, Notification } })
export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient) {
2019-05-01 15:07:08 +00:00
rest = getRest(this.auth, this.options.timeout)
2019-05-29 07:02:43 +00:00
statues = new AxiosLoadableMore<IStatus[], object>()
notifications = new AxiosLoadable<INotification[], object>()
emojis = new AxiosLoadable<Emoji[], object>()
stream?: WebSocket = undefined
2019-05-01 15:07:08 +00:00
2019-05-29 07:02:43 +00:00
targetStatus: IStatus | null = null
targetContext = new AxiosLoadable<Context, object>()
2019-05-27 13:44:49 +00:00
showCompose = false
compose: StatusPost = {
status: '',
visibility: 'unlisted',
2019-05-27 13:44:49 +00:00
sensitive: false,
spoiler_text: ''
}
2019-05-28 07:35:50 +00:00
showEmojis = false // MAYBE: show tabs with unicode emoticons
2019-05-27 13:44:49 +00:00
2019-05-01 15:07:08 +00:00
get hasNotifications() {
if(!this.notifications.isSuccess) {
return false
}
const not = this.notifications.get()
if(not){
return not.length > 0
} else {
return false
}
}
2019-05-29 07:02:43 +00:00
get hasContext() {
return this.targetStatus && !this.targetContext.hasError
}
2019-05-01 15:07:08 +00:00
created() {
this.$watch('options.timeline', this.init, { immediate: true })
2019-05-28 09:51:06 +00:00
this.notifications.load(this.get<INotification[]>('/notifications'))
this.emojis.load(this.get<Emoji[]>('/custom_emojis'), res => Lists.sort(res.data, e => e.shortcode, Lists.stringCompare))
}
init() {
this.statues.load(this.getTimeline())
2019-05-01 15:07:08 +00:00
this.setupStream()
}
2019-05-28 09:51:06 +00:00
get<T>(path: string, options = {}) {
return this.catchEmit(this.rest.get<T>(path, { params: { limit: this.options.buffer, ...options } }))
2019-05-01 15:07:08 +00:00
}
2019-05-28 09:51:06 +00:00
post<T>(path: string, options = {}) {
return this.catchEmit(this.rest.post<T>(path, options))
2019-05-01 15:07:08 +00:00
}
2019-05-28 07:35:50 +00:00
addEmoji(code: string) {
this.compose.status += `:${code}:`
}
2019-05-27 13:44:49 +00:00
sendStatus() {
if(this.compose.status) {
const post: StatusPost = {
status: this.compose.status,
visibility: this.compose.visibility
}
if(this.compose.sensitive) {
post.sensitive = true
if(this.compose.spoiler_text) {
post.spoiler_text = this.compose.spoiler_text
}
}
2019-05-29 07:02:43 +00:00
if(this.targetStatus) {
post.in_reply_to_id = this.targetStatus.id
}
2019-05-27 13:44:49 +00:00
this.post('/statuses', post)
this.compose.status = ''
}
}
getTimeline(options: any = {}) {
if (this.options.timeline === 'local') {
options.local = true
}
2019-05-28 09:51:06 +00:00
return this.get<IStatus[]>(`/timelines/${this.options.timeline === 'home' ? 'home' : 'public'}`, options)
2019-05-01 15:07:08 +00:00
}
onScroll(event: any) {
2019-05-02 16:28:00 +00:00
this.statues.handleScroll(event.target,
st => this.getTimeline({ max_id: Lists.last(st).id }),
(res, st) => Lists.pushAll(st, res.data),
st => Lists.removeFrom(st, this.options.buffer)
)
2019-05-01 15:07:08 +00:00
}
2019-05-03 15:03:13 +00:00
showStatus(status: IStatus) {
2019-05-01 15:07:08 +00:00
return (!status.in_reply_to_id || this.options.reply) && (!status.reblog || this.options.reblog)
}
2019-05-28 09:51:06 +00:00
onStatusMark(action: MarkStatus) {
this.post<IStatus>(`/statuses/${action.id}/${action.type}`)
.then(res => this.statues.with(
sts => Lists.setFirstBy(sts, st => st.id, action.id, res.data)
))
}
onPollVote(action: PollVote) {
this.post<Poll>(`/polls/${action.poll}/votes`, { choices: action.choices })
.then(res => this.statues.with(
2019-05-29 07:02:43 +00:00
sts => sts.find(st => st.id === action.id)!.poll = res.data
2019-05-28 09:51:06 +00:00
))
2019-05-01 15:07:08 +00:00
}
2019-05-29 07:02:43 +00:00
onShowContext(status: IStatus) {
this.statues.with(sts => {
this.targetStatus = status
this.targetContext.load(this.get(`/statuses/${status.id}/context`), undefined, true)
})
}
closeContext() {
this.targetStatus = null
this.targetContext.reset()
}
2019-05-01 15:07:08 +00:00
onNotificationDismiss(id: number) {
this.post('/notifications/dismiss', { id })
.then(() => this.notifications.with(
ns => Lists.removeFirstBy(ns, n => n.id, id)))
}
onNotificationsClear() {
this.post('/notifications/clear')
.then(() => this.notifications.with(
ns => Lists.clear(ns)))
}
setupStream() {
if(this.stream) {
this.stream.close()
}
2019-05-28 09:51:06 +00:00
this.get<{ version: string }>('/instance').then(res => {
2019-05-27 12:31:43 +00:00
const oldAuth = res.data.version < '2.8.4' ? `access_token=${this.auth.get(AUTH.TOKEN)}&` : ''
this.stream = new WebSocket(
`wss://${this.auth.get(AUTH.SERVER)}/api/v1/streaming?${oldAuth}stream=${STREAMS[this.options.timeline]}`,
2019-05-27 12:31:43 +00:00
this.auth.get(AUTH.TOKEN)
2019-05-01 15:07:08 +00:00
)
this.stream.onmessage = event => {
2019-05-27 12:31:43 +00:00
const data = JSON.parse(event.data)
const payload = JSON.parse(data.payload)
switch (data.event) {
case 'update':
this.statues.with(s => s.unshift(payload))
break
case 'notification':
this.notifications.with(n => n.unshift(payload))
break
case 'delete':
this.statues.with(st => Lists.removeFirstBy(st, s => s.id, payload.id))
break
}
2019-05-01 15:07:08 +00:00
}
this.stream.onerror = ev => this.emitError(ev.type)
this.stream.onclose = () => {
2019-05-27 12:31:43 +00:00
this.emitError(
'Mastodon stream disconnected !' +
(this.options.reconnect ? ' Reconnecting...' : '')
)
if (this.options.reconnect) {
setTimeout(() => this.setupStream(), this.options.timeout)
}
}
})
2019-05-01 15:07:08 +00:00
}
}
</script>
<style lang="sass">
2019-05-02 16:28:00 +00:00
@import '@/common.sass'
2019-05-01 15:07:08 +00:00
.mastodon
.client
display: flex
2019-05-27 13:44:49 +00:00
flex-direction: column
2019-05-01 15:07:08 +00:00
height: 100%
2019-05-27 13:44:49 +00:00
overflow: hidden
position: relative
2019-05-28 07:35:50 +00:00
.header, .emoji-list
2019-05-02 16:28:00 +00:00
@include main-tile
2019-05-29 07:02:43 +00:00
.header
margin-bottom: 0
2019-05-01 15:07:08 +00:00
.list
2019-05-02 16:28:00 +00:00
@include group-tile
2019-05-29 07:02:43 +00:00
flex-grow: 1
.statues, .notifications, .context, .emoji-list
flex-grow: 1
display: flex
flex-direction: column
2019-05-27 13:44:49 +00:00
overflow-y: auto
2019-05-29 07:02:43 +00:00
.ancestors, .descendants
.status
font-size: .9em
padding: $borderRadius
@include tile
2019-05-27 13:44:49 +00:00
.compose-toggle
position: absolute
bottom: .5em
right: 1.5em
background-color: $backColor
border: 1px solid $darkColor
border-radius: 100%
height: 2em
width: 2em
text-align: center
line-height: 2em
2019-05-28 07:35:50 +00:00
.emoji-list
img
width: 2em
height: 2em
2019-05-27 13:44:49 +00:00
.compose
@include main-tile
display: flex
min-height: 5em
textarea
flex: 1
.options
margin-right: 1em
2019-05-01 15:07:08 +00:00
.account
.name
margin: 0 $borderRadius
color: $foreColor
.avatar
float: left
2019-05-02 16:28:00 +00:00
@include rounded
2019-05-01 15:07:08 +00:00
width: $avatarSize
height: $avatarSize
background-size: $avatarSize $avatarSize
div
display: inline-block
.status, .notification
min-height: $avatarSize
.content
margin: .5em .5em .5em 1em
&.avatared
margin-left: .5em + $avatarSize
.reblog
font-size: .8em
.spoiler
margin-bottom: .5em
.media
margin: .5em
position: relative
display: inline-block
& > *
max-height: 10em
max-width: 100%
.gif
position: absolute
top: 0
bottom: 0
left: 0
right: 0
height: 100%
width: 100%
background-color: #00000044
color: white
padding: .5em
2019-05-28 09:51:06 +00:00
.card, .poll
2019-05-27 12:31:43 +00:00
@include tile
padding: .2em
display: block
.provider
float: right
2019-05-01 15:07:08 +00:00
.meta
margin-left: 1em + $avatarSize
font-size: .8em
2019-05-29 07:02:43 +00:00
.fil
float: right
2019-05-01 15:07:08 +00:00
a
margin: 0 .5em
.date, .dismiss
float: right
</style>