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" ) 👁
2019-05-27 14:24:42 +00:00
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'
2019-05-27 14:24:42 +00:00
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'
2019-05-27 14:24:42 +00:00
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 > ( )
2019-05-27 14:24:42 +00:00
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 : '' ,
2019-05-27 14:24:42 +00:00
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 ( ) {
2019-05-27 14:24:42 +00:00
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 ) )
2019-05-27 14:24:42 +00:00
}
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 = ''
}
}
2019-05-27 14:24:42 +00:00
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 ( ) {
2019-05-27 14:24:42 +00:00
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 ) } & ` : ''
2019-05-27 14:24:42 +00:00
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
)
2019-05-27 14:24:42 +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
}
2019-05-27 14:24:42 +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 : .9 em
padding : $borderRadius
@ include tile
2019-05-27 13:44:49 +00:00
. compose - toggle
position : absolute
bottom : .5 em
right : 1.5 em
background - color : $backColor
border : 1 px solid $darkColor
border - radius : 100 %
height : 2 em
width : 2 em
text - align : center
line - height : 2 em
2019-05-28 07:35:50 +00:00
. emoji - list
img
width : 2 em
height : 2 em
2019-05-27 13:44:49 +00:00
. compose
@ include main - tile
display : flex
min - height : 5 em
textarea
flex : 1
. options
margin - right : 1 em
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 : .5 em .5 em .5 em 1 em
& . avatared
margin - left : .5 em + $avatarSize
. reblog
font - size : .8 em
. spoiler
margin - bottom : .5 em
. media
margin : .5 em
position : relative
display : inline - block
& > *
max - height : 10 em
max - width : 100 %
. gif
position : absolute
top : 0
bottom : 0
left : 0
right : 0
height : 100 %
width : 100 %
background - color : # 00000044
color : white
padding : .5 em
2019-05-28 09:51:06 +00:00
. card , . poll
2019-05-27 12:31:43 +00:00
@ include tile
padding : .2 em
display : block
. provider
float : right
2019-05-01 15:07:08 +00:00
. meta
margin - left : 1 em + $avatarSize
font - size : .8 em
2019-05-29 07:02:43 +00:00
. fil
float : right
2019-05-01 15:07:08 +00:00
a
margin : 0 .5 em
. date , . dismiss
float : right
< / style >