Use bus and constants icons

This commit is contained in:
May B. 2019-05-31 11:03:25 +02:00
parent 95fa082b94
commit 271dd81d9e
10 changed files with 211 additions and 95 deletions

View File

@ -0,0 +1,14 @@
import Vue from 'vue'
import { Component, Prop } from 'vue-property-decorator'
export interface IEmit {
$emit: (name: string, arg: any) => void
}
@Component
export default class LocalBusMixin<O, B extends O & IEmit> extends Vue {
@Prop(Object)
readonly bus!: B
}

View File

@ -8,16 +8,20 @@ a.account(target="_blank" :href="account.url")
<script lang="ts">
import { Component, Mixins, Prop } from 'vue-property-decorator'
import ShowMediaMixin from '@/components/ShowMediaMixin'
import BusMixin from './BusMixin'
import { ParseEmojisMixin } from './ParseEmojisMixin'
import { Account as IAccount } from './Types'
@Component
export default class Account extends Mixins(ParseEmojisMixin, ShowMediaMixin) {
export default class Account extends Mixins(ParseEmojisMixin, BusMixin) {
@Prop(Object)
readonly account!: IAccount
get showMedia() {
return this.bus.showMedia
}
avatarStyle(avatar: string) {
return {
'background-image': `url(${avatar})`

View File

@ -0,0 +1,13 @@
import { Component } from 'vue-property-decorator'
import LocalBusMixin from '@/components/LocalBusMixin'
import { BusOptions } from './Types'
@Component
export default class BusMixin extends LocalBusMixin<BusOptions, any> { }
export const LocalEvents = {
Mark: 'mark',
Vote: 'vote',
Context: 'context'
}

View File

@ -4,34 +4,34 @@
.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" @context="onShowContext")
status(v-if="showStatus(status)" :key="status.id" :status="status" :bus="bus")
.status(v-show="statues.loadingMore")
.service-loader
.context(v-if="hasContext")
.header(@click="closeContext")
| Context
span.date(@click.stop.prevent="closeContext")
span.date(@click.stop.prevent="closeContext") {{ icons.close }}
.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")
status(v-for="status in targetContext.get().ancestors" :key="status.id" :status="status" :bus="bus")
.service-loader(v-else)
status.selected(:status="targetStatus" :showMedia="options.showMedia" @mark="onStatusMark" @vote="onPollVote" @context="closeContext")
status.selected(:status="targetStatus" :bus="bus")
.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")
status(v-for="status in targetContext.get().descendants" :key="status.id" :status="status" :bus="bus")
.service-loader(v-else)
.notifications(v-if="hasNotifications")
.header
| Notifications
span.date(@click.stop.prevent="onNotificationsClear")
span.date(@click.stop.prevent="onNotificationsClear") {{ icons.close }}
.list
notification(v-for="notification in notifications.get()" :key="notification.id" :notification="notification"
:showMedia="options.showMedia" @dismiss="onNotificationDismiss" @mark="onStatusMark" @vote="onPollVote" @context="onShowContext")
@dismiss="onNotificationDismiss" :bus="bus")
.compose-toggle(@click="showCompose = !showCompose") 🖉
.compose-toggle(@click="showCompose = !showCompose") {{ icons.compose }}
.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")
.compose(v-show="showCompose")
@ -48,26 +48,26 @@
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")
option(value="direct")
option(v-for="(icon, value) in visibilities" :value="value") {{ icon }}
span.note {{ compose.visibility }}
button(@click="sendStatus") Toot
</template>
<script lang="ts">
import axios, { AxiosResponse } from 'axios'
import { Component, Mixins, Prop } from 'vue-property-decorator'
import { Component, Mixins, Prop, Vue, Watch } from 'vue-property-decorator'
import LocalBusMixin from '@/components/LocalBusMixin'
import ServiceClient from '@/components/ServiceClient'
import Lists from '@/helpers/lists/Lists'
import AxiosLoadable from '@/helpers/loadable/AxiosLoadable'
import AxiosLoadableMore from '@/helpers/loadable/AxiosLoadableMore'
import { LocalEvents } from './BusMixin'
import { Icons, Visibility } from './Icons'
import { AUTH, getHeaders, getRest } from './Mastodon.vue'
import Notification from './Notification.vue'
import Status from './Status.vue'
import { Context, Emoji, MarkStatus, Notification as INotification, Options, Poll, PollVote, Status as IStatus, StatusPost, TimelineType } from './Types'
import { BusOptions, Context, Emoji, MarkStatus, Notification as INotification, Options, Poll, PollVote, Status as IStatus, StatusPost, TimelineType } from './Types'
const STREAMS = {
home: 'user',
@ -97,6 +97,11 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
}
showEmojis = false // MAYBE: show tabs with unicode emoticons
bus = new Vue({ data: {
showMedia: this.options.showMedia,
showCounts: this.options.showCounts
} })
get hasNotifications() {
if(!this.notifications.isSuccess) {
return false
@ -114,12 +119,31 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
return this.targetStatus && !this.targetContext.hasError
}
get visibilities() {
return Visibility
}
get icons() {
return Icons
}
created() {
this.$watch('options.timeline', this.init, { immediate: true })
new Map<string, (arg: any) => void>([
[ LocalEvents.Mark, this.onStatusMark ],
[ LocalEvents.Vote, this.onPollVote ],
[ LocalEvents.Context, this.onHandleContext ]
]).forEach((handler, name) => this.bus.$on(name, handler))
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))
}
@Watch('options', { deep: true })
change(o: any) {
Object.keys(this.bus.$data).forEach(key => this.bus.$data[key] = o[key])
}
@Watch('options.timeline', { immediate: true })
init() {
this.statues.load(this.getTimeline())
this.setupStream()
@ -190,7 +214,12 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
))
}
onShowContext(status: IStatus) {
onHandleContext(status: IStatus) {
if(this.targetStatus && this.targetStatus.id === status.id) {
this.closeContext()
return
}
this.statues.with(sts => {
this.targetStatus = status
this.targetContext.load(this.get(`/statuses/${status.id}/context`), undefined, true)

View File

@ -0,0 +1,25 @@
export const Visibility = {
public: '◍',
unlisted: '👁',
private: '⚿',
direct: '✉'
}
export const Notification = {
mention: '✉',
reblog: '⟳',
favourite: '⚝',
follow: '👁'
}
export const Icons = {
compose: '🖉',
close: '❌',
}
export const Context = {
full: '⮃',
up: '⮥',
down: '⮦',
no: '⭿'
}

View File

@ -5,13 +5,14 @@
| {{ serviceName }}:
loadable-inline(:loadable="account")
template(#success)
span(v-html="parseEmojis(account.data.display_name, account.data.emojis, params.showMedia) + '@' + server", params.showMedia)
span(v-html="parseEmojis(account.data.display_name, account.data.emojis, params.showMedia) + '@' + server")
template(#settings)
setting-boolean(:id="'reconnect'" :title="'Reconnect'" :value="params.reconnect" @change="saveOptionCouple")
setting-boolean(:id="'reblog'" :title="'Show reblogs'" :value="params.reblog" @change="saveOptionCouple")
setting-boolean(:id="'reply'" :title="'Show replies'" :value="params.reply" @change="saveOptionCouple")
setting-int(:id="'buffer'" :title="'Buffer size'" :value="params.buffer" @change="saveOptionCouple")
setting-boolean(:id="'showMedia'" :title="'Show medias'" :value="params.showMedia" @change="saveOptionCouple")
setting-boolean(:id="'showCounts'" :title="'Show counts'" :value="params.showCounts" @change="saveOptionCouple")
setting-select(:id="'timeline'" :title="'Timeline'" :value="params.timeline" @change="saveOptionCouple" :options="['home', 'local', 'public']")
loadable-block.service-content(:loadable="account")
template(#success)
@ -58,7 +59,7 @@ export default class Mastodon extends Mixins<AccountService<Account, object>>(Ac
get params(): Options {
return { timeout: 5000, reconnect: false, buffer: 20, reblog: true, reply: false,
showMedia: true, timeline: 'home', ...this.options }
showMedia: true, showCounts: true, timeline: 'home', ...this.options }
}
get isSetup() {

View File

@ -1,55 +1,46 @@
<template lang="pug">
.notification
account(:account="notification.account" :showMedia="showMedia")
span.colored.text-icon.letter(v-if="notification.type == 'mention'")
span.colored.text-icon.letter(v-if="notification.type == 'reblog'")
span.colored.text-icon.letter(v-if="notification.type == 'favourite'")
account(:account="notification.account" :bus="bus")
span.colored.text-icon.letter {{ notificationTypeIcon }}
span.date {{ fromNow(notification.created_at) }}
.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" @context="passContext")
:withAccount="notification.type != 'mention'" :bus="bus")
a.date(@click.stop.prevent="makeDismiss" style="margin-top: -1em")
a.date(@click.stop.prevent="makeDismiss" style="margin-top: -1em") {{ closeIcon }}
</template>
<script lang="ts">
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator'
import FromNowMixin from '@/components/FromNowMixin'
import ShowMediaMixin from '@/components/ShowMediaMixin'
import Account from './Account.vue'
import BusMixin from './BusMixin'
import { Icons, Notification as NotificationIcons } from './Icons'
import Status from './Status.vue'
import { MarkStatus, Notification as INotification, PollVote, Status as IStatus } from './Types'
@Component({ components: { Account, Status } })
export default class Notification extends Mixins(ShowMediaMixin, FromNowMixin) {
export default class Notification extends Mixins(FromNowMixin, BusMixin) {
@Prop(Object)
readonly notification!: INotification
get notificationTypeIcon() {
return NotificationIcons[this.notification.type] || '?'
}
get closeIcon() {
return Icons.close
}
@Emit('dismiss')
makeDismiss() {
return this.notification.id
}
@Emit('mark')
passMark(action: MarkStatus) {
return action
}
@Emit('vote')
passVote(action: PollVote) {
return action
}
@Emit('context')
passContext(status: IStatus) {
return status
}
}
</script>

View File

@ -1,8 +1,8 @@
<template lang="pug">
.status
account(v-if="withAccount" :account="status.account" :showMedia="showMedia")
account(v-if="withAccount" :account="status.account" :bus="bus")
span.text-icon.letter(v-if="status.reblog")
span.text-icon.letter(v-if="status.reblog") {{ reblogIcon }}
a.date(target="_blank" :href="status.uri") {{ fromNow(status.created_at) }}
@ -12,9 +12,10 @@
{{ status.spoiler_text || 'Spoiler' }} {{ status.sensitive ? '&rarr;' : '&darr;' }}
div(v-if="!status.spoiler_text || !status.sensitive")
.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)")
form.options(@submit.prevent="makeVote($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")
@ -23,12 +24,14 @@
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")
template(v-if="showMedia")
template(v-if="bus.showMedia")
img(v-if="media.type == 'image' || media.type == 'gifv'" :src="media.preview_url" :alt="media.description" :title="media.description")
template(v-else) Wrong type
.gif(v-if="media.type == 'gifv'") GIF
template(v-else) Hidden media {{ media.description }}
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 }}
.title {{ status.card.title }}
@ -36,38 +39,29 @@
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" @context="passContext")
status.reblog(v-else :status="status.reblog" :bus="bus")
.meta(v-if="!status.reblog")
a.replies(@click.stop.prevent="makeReply(status)")
span.text-icon
| {{ status.replies_count }}
a.reblogs(:class="{ colored: status.reblogged }" @click.stop.prevent="makeReblog(status)")
span.text-icon
| {{ status.reblogs_count }}
a.favourites(:class="{ colored: status.favourited }" @click.stop.prevent="makeFav(status)")
span.text-icon
| {{ status.favourites_count }}
a.visibility
template(v-if="status.visibility == 'public'")
template(v-else-if="status.visibility == 'unlisted'") 👁
template(v-else-if="status.visibility == 'private'")
template(v-else-if="status.visibility == 'direct'")
a.fil(@click.stop.prevent="passContext(status)")
span.text-icon
status-meta(v-for="meta in metas" :key="meta.name" :meta="meta" :bus="bus")
a {{ statusVisibilityIcon }}
a.fil(@click.stop.prevent="showContext")
| {{ contextIcon }}
</template>
<script lang="ts">
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator'
import FromNowMixin from '@/components/FromNowMixin'
import ShowMediaMixin from '@/components/ShowMediaMixin'
import Account from './Account.vue'
import BusMixin, { LocalEvents } from './BusMixin'
import { Context, Notification, Visibility } from './Icons'
import { ParseEmojisMixin } from './ParseEmojisMixin'
import StatusMeta from './StatusMeta.vue'
import { Card, MarkStatus, MarkStatusType, Poll, PollVote, Status as IStatus } from './Types'
@Component({ components: { Account } })
export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, FromNowMixin) {
@Component({ components: { Account, StatusMeta } })
export default class Status extends Mixins(ParseEmojisMixin, FromNowMixin, BusMixin) {
@Prop(Object)
readonly status!: IStatus
@ -75,44 +69,61 @@ export default class Status extends Mixins(ParseEmojisMixin, ShowMediaMixin, Fro
@Prop({ type: Boolean, default: true })
readonly withAccount!: boolean
@Emit('mark')
passMark(action: MarkStatus) {
return action
get statusVisibilityIcon() {
return Visibility[this.status.visibility] || '?'
}
@Emit('vote')
passVote(action: PollVote) {
return action
get reblogIcon() {
return Notification.reblog
}
@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.passVote({ id: status.id, poll: status.poll!.id, choices })
get contextIcon() {
if(this.status.in_reply_to_id) {
return Context[this.status.replies_count ? 'full' : 'up']
} else {
return Context[this.status.replies_count ? 'down' : 'no']
}
}
makeMark(status: IStatus, action: string, undo: boolean) {
this.passMark({
id: status.id, type: (undo ? 'un' : '') + action as MarkStatusType
get showMedia() {
return this.bus.$data.showMedia
}
get metas() {
return [
{ name: 'reply', click: this.makeReply, active: false, icon: Notification.mention, count: this.status.replies_count },
{ name: 'reblog', click: this.makeReblog, active: this.status.reblogged, icon: Notification.reblog, count: this.status.reblogs_count },
{ name: 'fav', click: this.makeFav, active: this.status.favourited, icon: Notification.favourite, count: this.status.favourites_count }
]
}
makeVote(elements: HTMLInputElement[]) {
const choices = Object.values(elements).filter(e => e.checked).map(e => e.value)
if(choices.length > 0) {
this.bus.$emit(LocalEvents.Vote, { id: this.status.id, poll: this.status.poll!.id, choices })
}
}
makeMark(action: string, undo: boolean) {
this.bus.$emit(LocalEvents.Mark, {
id: this.status.id, type: (undo ? 'un' : '') + action as MarkStatusType
})
}
makeReblog(status: IStatus) {
this.makeMark(status, 'reblog', status.reblogged)
makeReblog() {
this.makeMark('reblog', this.status.reblogged)
}
makeFav(status: IStatus) {
this.makeMark(status, 'favourite', status.favourited)
makeFav() {
this.makeMark('favourite', this.status.favourited)
}
makeReply(status: IStatus) {
throw status.id // TODO:
showContext() {
this.bus.$emit(LocalEvents.Context, this.status)
}
makeReply() {
throw this // TODO:
}
}

View File

@ -0,0 +1,23 @@
<template lang="pug">
a(:class="{ colored: meta.active }" @click.stop.prevent="meta.click")
span.text-icon {{ meta.icon }}
template(v-if="showCounts") {{ meta.count }}
</template>
<script lang="ts">
import { Component, Mixins, Prop } from 'vue-property-decorator'
import BusMixin from './BusMixin'
@Component
export default class StatusMeta extends Mixins(BusMixin) {
@Prop(Object)
readonly meta!: object
get showCounts() {
return this.bus.$data.showCounts
}
}
</script>

View File

@ -11,13 +11,17 @@ export interface Account {
export type TimelineType = 'home' | 'local' | 'public'
export interface Options {
export interface BusOptions {
showMedia: boolean
showCounts: boolean
}
export interface Options extends BusOptions {
timeout: number
reconnect: boolean
buffer: number
reblog: boolean
reply: boolean
showMedia: boolean
timeline: TimelineType
}
@ -99,10 +103,11 @@ export interface Context {
descendants: Status[]
}
export type NotificationType = 'follow' | 'mention' | 'reblog' | 'favourite'
export interface Notification {
id: number
account: Account
type: string
type: NotificationType
created_at: string
status?: Status
}