Mastodon: Polls and better marks
This commit is contained in:
parent
cb5c6ccb22
commit
6e74ef10b3
|
@ -19,7 +19,11 @@ export default class FromNowMixin extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
fromNow(date: Date | number | string) {
|
fromNow(date: Date | number | string) {
|
||||||
const milliseconds = Math.floor(FromNowMixin.toNumber(TimeModule.now) - FromNowMixin.toNumber(date))
|
const now = FromNowMixin.toNumber(TimeModule.now)
|
||||||
|
const target = FromNowMixin.toNumber(date)
|
||||||
|
|
||||||
|
const prefix = target > now ? 'in ' : ''
|
||||||
|
const milliseconds = Math.floor(Math.abs(target - now))
|
||||||
|
|
||||||
let cur = 0
|
let cur = 0
|
||||||
let divider = 1
|
let divider = 1
|
||||||
|
@ -28,10 +32,11 @@ export default class FromNowMixin extends Vue {
|
||||||
divider *= time[1]
|
divider *= time[1]
|
||||||
const next = Math.floor(milliseconds / divider)
|
const next = Math.floor(milliseconds / divider)
|
||||||
if(next <= 0) {
|
if(next <= 0) {
|
||||||
return `${cur} ${name}${cur > 1 ? 's' : ''}`
|
return `${prefix}${cur} ${name}${cur > 1 ? 's' : ''}`
|
||||||
}
|
}
|
||||||
name = time[0]
|
name = time[0]
|
||||||
cur = next
|
cur = next
|
||||||
}
|
}
|
||||||
|
return `${prefix}a long time`
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -33,7 +33,7 @@ export default class ServiceEmiter extends Vue {
|
||||||
this.emit(Events.SaveServiceEvent, service)
|
this.emit(Events.SaveServiceEvent, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
catchEmit(req: AxiosPromise) {
|
catchEmit<T>(req: AxiosPromise<T>) {
|
||||||
return req.catch(err => {
|
return req.catch(err => {
|
||||||
this.emitError(err)
|
this.emitError(err)
|
||||||
throw err
|
throw err
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default abstract class Lists {
|
export default abstract class Lists {
|
||||||
|
|
||||||
static last<T>(list: T[]) {
|
static last<T>(list: T[]) {
|
||||||
|
@ -13,6 +15,18 @@ export default abstract class Lists {
|
||||||
return list.length
|
return list.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static setAt<T>(list: T[], id: number, val: T) {
|
||||||
|
return Vue.set(list, id, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
static setFirst<T>(list: T[], where: (val: T) => boolean, val: T) {
|
||||||
|
return this.setAt(list, this.getIndex(list, where), val)
|
||||||
|
}
|
||||||
|
|
||||||
|
static setFirstBy<T, U>(list: T[], mapper: (val: T) => U, key: U, val: T) {
|
||||||
|
return this.setFirst(list, e => mapper(e) === key, val)
|
||||||
|
}
|
||||||
|
|
||||||
static removeAt<T>(list: T[], id: number) {
|
static removeAt<T>(list: T[], id: number) {
|
||||||
list.splice(id, 1)
|
list.splice(id, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.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")
|
status(v-if="showStatus(status)" :key="status.id" :status="status" :showMedia="options.showMedia" @mark="onStatusMark" @vote="onPollVote")
|
||||||
.status(v-show="statues.loadingMore")
|
.status(v-show="statues.loadingMore")
|
||||||
.service-loader
|
.service-loader
|
||||||
.notifications(v-if="hasNotifications")
|
.notifications(v-if="hasNotifications")
|
||||||
|
@ -13,9 +13,9 @@
|
||||||
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")
|
:showMedia="options.showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark" @vote="onPollVote")
|
||||||
.compose-toggle(@click="showCompose = !showCompose") 🖉
|
.compose-toggle(@click="showCompose = !showCompose") 🖉
|
||||||
.emoji-list(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")
|
||||||
.compose(v-show="showCompose")
|
.compose(v-show="showCompose")
|
||||||
textarea.content(v-model="compose.status" placeholder="message")
|
textarea.content(v-model="compose.status" placeholder="message")
|
||||||
|
@ -50,7 +50,7 @@ import AxiosLodableMore 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, MarkMessage, Notification as INotification, Options, Status as IStatus, StatusPost, TimelineType } from './Types'
|
import { Emoji, MarkStatus, Notification as INotification, Options, Poll, PollVote, Status as IStatus, StatusPost, TimelineType } from './Types'
|
||||||
|
|
||||||
const STREAMS = {
|
const STREAMS = {
|
||||||
home: 'user',
|
home: 'user',
|
||||||
|
@ -92,8 +92,8 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.$watch('options.timeline', this.init, { immediate: true })
|
this.$watch('options.timeline', this.init, { immediate: true })
|
||||||
this.notifications.load(this.get('/notifications'))
|
this.notifications.load(this.get<INotification[]>('/notifications'))
|
||||||
this.emojis.load<Emoji[]>(this.get('/custom_emojis'), res => Lists.sort(res.data, e => e.shortcode, Lists.stringCompare))
|
this.emojis.load(this.get<Emoji[]>('/custom_emojis'), res => Lists.sort(res.data, e => e.shortcode, Lists.stringCompare))
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -101,12 +101,12 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
this.setupStream()
|
this.setupStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
get(path: string, options = {}) {
|
get<T>(path: string, options = {}) {
|
||||||
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
|
return this.catchEmit(this.rest.get<T>(path, { params: { limit: this.options.buffer, ...options } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
post(path: string, options = {}) {
|
post<T>(path: string, options = {}) {
|
||||||
return this.catchEmit(this.rest.post(path, options))
|
return this.catchEmit(this.rest.post<T>(path, options))
|
||||||
}
|
}
|
||||||
|
|
||||||
addEmoji(code: string) {
|
addEmoji(code: string) {
|
||||||
|
@ -134,7 +134,7 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
if (this.options.timeline === 'local') {
|
if (this.options.timeline === 'local') {
|
||||||
options.local = true
|
options.local = true
|
||||||
}
|
}
|
||||||
return this.get(`/timelines/${this.options.timeline === 'home' ? 'home' : 'public'}`, options)
|
return this.get<IStatus[]>(`/timelines/${this.options.timeline === 'home' ? 'home' : 'public'}`, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll(event: any) {
|
onScroll(event: any) {
|
||||||
|
@ -149,9 +149,18 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
return (!status.in_reply_to_id || this.options.reply) && (!status.reblog || this.options.reblog)
|
return (!status.in_reply_to_id || this.options.reply) && (!status.reblog || this.options.reblog)
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatusMark(action: MarkMessage) {
|
onStatusMark(action: MarkStatus) {
|
||||||
this.post(`/statuses/${action.id}/${action.type}`)
|
this.post<IStatus>(`/statuses/${action.id}/${action.type}`)
|
||||||
.then(() => action.callback())
|
.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(
|
||||||
|
sts => sts.find(st => st.id == action.id)!.poll = res.data
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
onNotificationDismiss(id: number) {
|
onNotificationDismiss(id: number) {
|
||||||
|
@ -170,7 +179,7 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
if(this.stream) {
|
if(this.stream) {
|
||||||
this.stream.close()
|
this.stream.close()
|
||||||
}
|
}
|
||||||
this.get('/instance').then(res => {
|
this.get<{ version: string }>('/instance').then(res => {
|
||||||
const oldAuth = res.data.version < '2.8.4' ? `access_token=${this.auth.get(AUTH.TOKEN)}&` : ''
|
const oldAuth = res.data.version < '2.8.4' ? `access_token=${this.auth.get(AUTH.TOKEN)}&` : ''
|
||||||
this.stream = new WebSocket(
|
this.stream = new WebSocket(
|
||||||
`wss://${this.auth.get(AUTH.SERVER)}/api/v1/streaming?${oldAuth}stream=${STREAMS[this.options.timeline]}`,
|
`wss://${this.auth.get(AUTH.SERVER)}/api/v1/streaming?${oldAuth}stream=${STREAMS[this.options.timeline]}`,
|
||||||
|
@ -291,7 +300,7 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
background-color: #00000044
|
background-color: #00000044
|
||||||
color: white
|
color: white
|
||||||
padding: .5em
|
padding: .5em
|
||||||
.card
|
.card, .poll
|
||||||
@include tile
|
@include tile
|
||||||
padding: .2em
|
padding: .2em
|
||||||
display: block
|
display: block
|
||||||
|
|
|
@ -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")
|
:showMedia="showMedia" :withAccount="notification.type != 'mention'" @mark="passMark" @vote="passVote")
|
||||||
|
|
||||||
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 { MarkMessage, Notification as INotification } from './Types'
|
import { MarkStatus, Notification as INotification, PollVote } 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) {
|
||||||
|
@ -37,7 +37,12 @@ export default class Notification extends Mixins(ShowMediaMixin, FromNowMixin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Emit('mark')
|
@Emit('mark')
|
||||||
passMark(action: MarkMessage) {
|
passMark(action: MarkStatus) {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
@Emit('vote')
|
||||||
|
passVote(action: PollVote) {
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,23 @@
|
||||||
{{ status.spoiler_text || 'Spoiler' }} {{ status.sensitive ? '→' : '↓' }}
|
{{ status.spoiler_text || 'Spoiler' }} {{ status.sensitive ? '→' : '↓' }}
|
||||||
div(v-if="!status.spoiler_text || !status.sensitive")
|
div(v-if="!status.spoiler_text || !status.sensitive")
|
||||||
.text(v-html="parseEmojis(status.content, status.emojis, showMedia)")
|
.text(v-html="parseEmojis(status.content, status.emojis, showMedia)")
|
||||||
|
.poll(v-if="status.poll")
|
||||||
|
.date {{ fromNow(status.poll.expires_at) }}
|
||||||
|
form.options(@submit.prevent="makeVote(status, $event.target.elements)")
|
||||||
|
.option(v-for="option in status.poll.options")
|
||||||
|
input(v-if="!status.poll.expired && !status.poll.voted" :type="status.poll.multiple ? 'checkbox' : 'radio'" :id="status.poll.id + option.title" :value="option.title" :name="status.poll.id")
|
||||||
|
label(:for="status.poll.id + option.title")
|
||||||
|
| {{ option.title }}
|
||||||
|
span.note {{ option.votes_count }}
|
||||||
|
button(v-if="status.poll.voted") voted
|
||||||
|
button(v-else-if="status.poll.expired") expired
|
||||||
|
input(v-else type="submit" value="vote")
|
||||||
a.media(v-for="media in status.media_attachments" :href="media.url" target="_blank")
|
a.media(v-for="media in status.media_attachments" :href="media.url" target="_blank")
|
||||||
template(v-if="showMedia")
|
template(v-if="showMedia")
|
||||||
img(v-if="media.type == 'image' || media.type == 'gifv'" :src="media.preview_url" :alt="media.description" :title="media.description")
|
img(v-if="media.type == 'image' || media.type == 'gifv'" :src="media.preview_url" :alt="media.description" :title="media.description")
|
||||||
template(v-else) Wrong type
|
template(v-else) Wrong type
|
||||||
.gif(v-if="media.type == 'gifv'") GIF
|
.gif(v-if="media.type == 'gifv'") GIF
|
||||||
template(v-else) Hidden media {{ media.description }}
|
template(v-else) Hidden media {{ media.description }}
|
||||||
.poll(v-if="status.poll") {{ renderPoll(status.poll) }}
|
|
||||||
a.card(v-if="status.card" :href="status.card.url" target="_blank")
|
a.card(v-if="status.card" :href="status.card.url" target="_blank")
|
||||||
a.provider(v-if="status.card.provider_name" :src="status.card.provider_url" target="_blank") {{ status.card.provider_name }}
|
a.provider(v-if="status.card.provider_name" :src="status.card.provider_url" target="_blank") {{ status.card.provider_name }}
|
||||||
.title {{ status.card.title }}
|
.title {{ status.card.title }}
|
||||||
|
@ -26,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")
|
status.reblog(v-else :status="status.reblog" :showMedia="showMedia" @mark="passMark" @vote="passVote")
|
||||||
|
|
||||||
.meta(v-if="!status.reblog")
|
.meta(v-if="!status.reblog")
|
||||||
a.replies(@click.stop.prevent="makeReply(status)")
|
a.replies(@click.stop.prevent="makeReply(status)")
|
||||||
|
@ -54,7 +64,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 { ParseEmojisMixin } from './ParseEmojisMixin'
|
import { ParseEmojisMixin } from './ParseEmojisMixin'
|
||||||
import { Card, MarkMessage, Poll, Status as IStatus } from './Types'
|
import { Card, MarkStatus, MarkStatusType, Poll, PollVote, Status as IStatus } from './Types'
|
||||||
|
|
||||||
@Component({ components: { Account } })
|
@Component({ components: { Account } })
|
||||||
export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, FromNowMixin) {
|
export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, FromNowMixin) {
|
||||||
|
@ -73,31 +83,43 @@ export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, Fro
|
||||||
throw status.id // TODO:
|
throw status.id // TODO:
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPoll(poll: Poll) {
|
@Emit('mark')
|
||||||
throw poll // TODO:
|
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')
|
||||||
emitMark(status: IStatus, action: 'reblog' | 'favourite', callback: CallableFunction, undo = false): MarkMessage {
|
passMark(action: MarkStatus) {
|
||||||
return {
|
return action
|
||||||
id: status.id,
|
}
|
||||||
type: (undo ? 'un' : '') + action,
|
|
||||||
callback
|
@Emit('vote')
|
||||||
|
passVote(action: PollVote) {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makeReblog(status: IStatus) {
|
makeReblog(status: IStatus) {
|
||||||
this.emitMark(status, 'reblog', () => {
|
this.emitMark(status.id, 'reblog', status.reblogged)
|
||||||
status.reblogs_count += (status.reblogged ? -1 : 1)
|
|
||||||
status.reblogged = !status.reblogged
|
|
||||||
}, status.reblogged)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
makeFav(status: IStatus) {
|
makeFav(status: IStatus) {
|
||||||
this.emitMark(status, 'favourite', () => {
|
this.emitMark(status.id, 'favourite', status.favourited)
|
||||||
status.favourites_count += (status.favourited ? -1 : 1)
|
|
||||||
status.favourited = !status.favourited
|
|
||||||
}, status.favourited)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,12 @@ export interface Poll {
|
||||||
voted?: boolean
|
voted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PollVote {
|
||||||
|
id: number
|
||||||
|
poll: string
|
||||||
|
choices: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface Media {
|
export interface Media {
|
||||||
description: string
|
description: string
|
||||||
url: string
|
url: string
|
||||||
|
@ -96,8 +102,8 @@ export interface Notification {
|
||||||
status?: Status
|
status?: Status
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkMessage {
|
export type MarkStatusType = 'reblog' | 'unreblog' | 'favourite' | 'unfavourite'
|
||||||
|
export interface MarkStatus {
|
||||||
id: number,
|
id: number,
|
||||||
type: string,
|
type: MarkStatusType
|
||||||
callback: CallableFunction
|
|
||||||
}
|
}
|
|
@ -94,7 +94,7 @@ export default class NextcloudNews extends Mixins<ConnectedService<object, objec
|
||||||
|
|
||||||
loadData() {
|
loadData() {
|
||||||
this.news.load<{ items: News[] }>(
|
this.news.load<{ items: News[] }>(
|
||||||
this.catchEmit(this.rest.get<News[]>('/items', { params: { batchSize: this.params.buffer, type: 3, getRead: false } })),
|
this.catchEmit(this.rest.get<{ items: News[] }>('/items', { params: { batchSize: this.params.buffer, type: 3, getRead: false } })),
|
||||||
res => res.data.items.map(n => {
|
res => res.data.items.map(n => {
|
||||||
n.open = false
|
n.open = false
|
||||||
return n
|
return n
|
||||||
|
|
Loading…
Reference in New Issue