Mastodon: Context

This commit is contained in:
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> <noscript>
<strong>We're sorry but mixit doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but mixit doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript> </noscript>
<div id="app"></div> <main id="app"></main>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@
.content .content
template(v-if="notification.type == 'follow'") Vous suit template(v-if="notification.type == 'follow'") Vous suit
status.reblog(v-else-if="notification.status" :status="notification.status" 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") a.date(@click.stop.prevent="makeDismiss" style="margin-top: -1em")
</template> </template>
@ -23,7 +23,7 @@ import FromNowMixin from '@/components/FromNowMixin'
import ShowMediaMixin from '@/components/ShowMediaMixin' import ShowMediaMixin from '@/components/ShowMediaMixin'
import Account from './Account.vue' import Account from './Account.vue'
import Status from './Status.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 } }) @Component({ components: { Account, Status } })
export default class Notification extends Mixins(ShowMediaMixin, FromNowMixin) { export default class Notification extends Mixins(ShowMediaMixin, FromNowMixin) {
@ -46,5 +46,10 @@ export default class Notification extends Mixins(ShowMediaMixin, FromNowMixin) {
return action return action
} }
@Emit('context')
passContext(status: IStatus) {
return status
}
} }
</script> </script>

View File

@ -36,7 +36,7 @@
template(v-if="status.card.image") template(v-if="status.card.image")
img(v-if="showMedia" :src="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 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") .meta(v-if="!status.reblog")
a.replies(@click.stop.prevent="makeReply(status)") a.replies(@click.stop.prevent="makeReply(status)")
@ -53,8 +53,8 @@
template(v-else-if="status.visibility == 'unlisted'") 👁 template(v-else-if="status.visibility == 'unlisted'") 👁
template(v-else-if="status.visibility == 'private'") template(v-else-if="status.visibility == 'private'")
template(v-else-if="status.visibility == 'direct'") 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)") a.fil(@click.stop.prevent="passContext(status)")
| Voir le fil span.text-icon
</template> </template>
<script lang="ts"> <script lang="ts">
@ -75,28 +75,6 @@ export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, Fro
@Prop({ type: Boolean, default: true }) @Prop({ type: Boolean, default: true })
readonly withAccount!: boolean 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') @Emit('mark')
passMark(action: MarkStatus) { passMark(action: MarkStatus) {
return action return action
@ -107,19 +85,34 @@ export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, Fro
return action return action
} }
@Emit('context')
passContext(status: IStatus) {
return status
}
makeVote(status: IStatus, elements: HTMLInputElement[]) { makeVote(status: IStatus, elements: HTMLInputElement[]) {
const choices = Object.values(elements).filter(e => e.checked).map(e => e.value) const choices = Object.values(elements).filter(e => e.checked).map(e => e.value)
if(choices.length > 0) { 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) { makeReblog(status: IStatus) {
this.emitMark(status.id, 'reblog', status.reblogged) this.makeMark(status, 'reblog', status.reblogged)
} }
makeFav(status: IStatus) { 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 type: string
} }
export interface Context {
ancestors: Status[]
descendants: Status[]
}
export interface Notification { export interface Notification {
id: number id: number
account: Account account: Account