Mastodon: Context

master
May B. 2019-05-29 09:02:43 +02:00
parent 6e74ef10b3
commit 95fa082b94
8 changed files with 106 additions and 43 deletions

View File

@ -10,7 +10,7 @@
<noscript>
<strong>We're sorry but mixit doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<main id="app"></main>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -340,6 +340,7 @@ input, select, button, textarea
float: right
width: 1.2em
.service-content
flex-grow: 1
overflow: hidden
.service-loader
display: inline-block

View File

@ -9,6 +9,10 @@ export default class ErrorLoadable<T, E> extends Loadable<T> {
this.error = undefined
}
get hasError() {
return this.error !== undefined
}
get isSuccess() {
return this.loaded && this.error === undefined
}

View File

@ -9,6 +9,10 @@ export default class Loadable<T> {
this.reset()
}
get isLoaded() {
return this.loaded
}
reset() {
this.loaded = false
this.data = undefined

View File

@ -1,19 +1,36 @@
<template lang="pug">
.client
.statues(@scroll.passive="onScroll")
.statues(@scroll.passive="onScroll" v-show="!hasContext")
.header(v-if="hasNotifications") Accueil
success-loadable.list(:loadable="statues")
template(v-for="status in statues.get()")
status(v-if="showStatus(status)" :key="status.id" :status="status" :showMedia="options.showMedia" @mark="onStatusMark" @vote="onPollVote")
status(v-if="showStatus(status)" :key="status.id" :status="status" :showMedia="options.showMedia" @mark="onStatusMark" @vote="onPollVote" @context="onShowContext")
.status(v-show="statues.loadingMore")
.service-loader
.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)
.notifications(v-if="hasNotifications")
.header
| Notifications
span.date(@click.stop.prevent="onNotificationsClear")
.list
notification(v-for="notification in notifications.get()" :key="notification.id" :notification="notification"
:showMedia="options.showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark" @vote="onPollVote")
:showMedia="options.showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark" @vote="onPollVote" @context="onShowContext")
.compose-toggle(@click="showCompose = !showCompose") 🖉
.emoji-list(v-if="options.showMedia" v-show="showCompose && showEmojis")
img.emoji(v-for="emoji in emojis.get()" @click="addEmoji(emoji.shortcode)" :src="emoji.static_url" :alt="emoji.shortcode" :title="emoji.shortcode")
@ -45,12 +62,12 @@ import { Component, Mixins, Prop } from 'vue-property-decorator'
import ServiceClient from '@/components/ServiceClient'
import Lists from '@/helpers/lists/Lists'
import AxiosLodable from '@/helpers/loadable/AxiosLoadable'
import AxiosLodableMore from '@/helpers/loadable/AxiosLoadableMore'
import AxiosLoadable from '@/helpers/loadable/AxiosLoadable'
import AxiosLoadableMore from '@/helpers/loadable/AxiosLoadableMore'
import { AUTH, getHeaders, getRest } from './Mastodon.vue'
import Notification from './Notification.vue'
import Status from './Status.vue'
import { Emoji, MarkStatus, Notification as INotification, Options, Poll, PollVote, Status as IStatus, StatusPost, TimelineType } from './Types'
import { Context, Emoji, MarkStatus, Notification as INotification, Options, Poll, PollVote, Status as IStatus, StatusPost, TimelineType } from './Types'
const STREAMS = {
home: 'user',
@ -63,11 +80,14 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
rest = getRest(this.auth, this.options.timeout)
statues = new AxiosLodableMore<IStatus[], object>()
notifications = new AxiosLodable<INotification[], object>()
emojis = new AxiosLodable<Emoji[], object>()
statues = new AxiosLoadableMore<IStatus[], object>()
notifications = new AxiosLoadable<INotification[], object>()
emojis = new AxiosLoadable<Emoji[], object>()
stream?: WebSocket = undefined
targetStatus: IStatus | null = null
targetContext = new AxiosLoadable<Context, object>()
showCompose = false
compose: StatusPost = {
status: '',
@ -90,6 +110,10 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
}
}
get hasContext() {
return this.targetStatus && !this.targetContext.hasError
}
created() {
this.$watch('options.timeline', this.init, { immediate: true })
this.notifications.load(this.get<INotification[]>('/notifications'))
@ -125,6 +149,9 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
post.spoiler_text = this.compose.spoiler_text
}
}
if(this.targetStatus) {
post.in_reply_to_id = this.targetStatus.id
}
this.post('/statuses', post)
this.compose.status = ''
}
@ -159,10 +186,22 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
onPollVote(action: PollVote) {
this.post<Poll>(`/polls/${action.poll}/votes`, { choices: action.choices })
.then(res => this.statues.with(
sts => sts.find(st => st.id == action.id)!.poll = res.data
sts => sts.find(st => st.id === action.id)!.poll = res.data
))
}
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()
}
onNotificationDismiss(id: number) {
this.post('/notifications/dismiss', { id })
.then(() => this.notifications.with(
@ -230,11 +269,21 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
position: relative
.header, .emoji-list
@include main-tile
.header
margin-bottom: 0
.list
@include group-tile
.statues, .notifications, .emoji-list
flex: 1
flex-grow: 1
.statues, .notifications, .context, .emoji-list
flex-grow: 1
display: flex
flex-direction: column
overflow-y: auto
.ancestors, .descendants
.status
font-size: .9em
padding: $borderRadius
@include tile
.compose-toggle
position: absolute
bottom: .5em
@ -309,6 +358,8 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
.meta
margin-left: 1em + $avatarSize
font-size: .8em
.fil
float: right
a
margin: 0 .5em

View File

@ -11,7 +11,7 @@
.content
template(v-if="notification.type == 'follow'") Vous suit
status.reblog(v-else-if="notification.status" :status="notification.status"
:showMedia="showMedia" :withAccount="notification.type != 'mention'" @mark="passMark" @vote="passVote")
:showMedia="showMedia" :withAccount="notification.type != 'mention'" @mark="passMark" @vote="passVote" @context="passContext")
a.date(@click.stop.prevent="makeDismiss" style="margin-top: -1em")
</template>
@ -23,7 +23,7 @@ import FromNowMixin from '@/components/FromNowMixin'
import ShowMediaMixin from '@/components/ShowMediaMixin'
import Account from './Account.vue'
import Status from './Status.vue'
import { MarkStatus, Notification as INotification, PollVote } from './Types'
import { MarkStatus, Notification as INotification, PollVote, Status as IStatus } from './Types'
@Component({ components: { Account, Status } })
export default class Notification extends Mixins(ShowMediaMixin, FromNowMixin) {
@ -46,5 +46,10 @@ export default class Notification extends Mixins(ShowMediaMixin, FromNowMixin) {
return action
}
@Emit('context')
passContext(status: IStatus) {
return status
}
}
</script>

View File

@ -36,7 +36,7 @@
template(v-if="status.card.image")
img(v-if="showMedia" :src="status.card.image")
a(v-else-if="status.card.type == 'photo'" :src="status.card.image" target="_blank") Hidden media
status.reblog(v-else :status="status.reblog" :showMedia="showMedia" @mark="passMark" @vote="passVote")
status.reblog(v-else :status="status.reblog" :showMedia="showMedia" @mark="passMark" @vote="passVote" @context="passContext")
.meta(v-if="!status.reblog")
a.replies(@click.stop.prevent="makeReply(status)")
@ -53,8 +53,8 @@
template(v-else-if="status.visibility == 'unlisted'") 👁
template(v-else-if="status.visibility == 'private'")
template(v-else-if="status.visibility == 'direct'")
a.fil(v-if="status.in_reply_to_id" @click.stop.prevent="showReply(status.in_reply_to_id)")
| Voir le fil
a.fil(@click.stop.prevent="passContext(status)")
span.text-icon
</template>
<script lang="ts">
@ -75,28 +75,6 @@ export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, Fro
@Prop({ type: Boolean, default: true })
readonly withAccount!: boolean
showReply(statusId: number) {
throw statusId // TODO:
}
makeReply(status: IStatus) {
throw status.id // TODO:
}
@Emit('mark')
emitMark(id: number, action: 'reblog' | 'favourite', undo = false): MarkStatus {
return {
id, type: (undo ? 'un' : '') + action as MarkStatusType
}
}
@Emit('vote')
emitVote(id: number, poll: string, choices: string[]): PollVote {
return {
id, poll, choices
}
}
@Emit('mark')
passMark(action: MarkStatus) {
return action
@ -107,19 +85,34 @@ export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, Fro
return action
}
@Emit('context')
passContext(status: IStatus) {
return status
}
makeVote(status: IStatus, elements: HTMLInputElement[]) {
const choices = Object.values(elements).filter(e => e.checked).map(e => e.value)
if(choices.length > 0) {
this.emitVote(status.id, status.poll!.id, choices)
this.passVote({ id: status.id, poll: status.poll!.id, choices })
}
}
makeMark(status: IStatus, action: string, undo: boolean) {
this.passMark({
id: status.id, type: (undo ? 'un' : '') + action as MarkStatusType
})
}
makeReblog(status: IStatus) {
this.emitMark(status.id, 'reblog', status.reblogged)
this.makeMark(status, 'reblog', status.reblogged)
}
makeFav(status: IStatus) {
this.emitMark(status.id, 'favourite', status.favourited)
this.makeMark(status, 'favourite', status.favourited)
}
makeReply(status: IStatus) {
throw status.id // TODO:
}
}

View File

@ -94,6 +94,11 @@ export interface Media {
type: string
}
export interface Context {
ancestors: Status[]
descendants: Status[]
}
export interface Notification {
id: number
account: Account