Add discord (WIP)
This commit is contained in:
parent
ddb4890c15
commit
9f777238da
11
src/App.vue
11
src/App.vue
|
@ -266,7 +266,7 @@ body
|
||||||
background-color: $backColor
|
background-color: $backColor
|
||||||
color: $foreColor
|
color: $foreColor
|
||||||
|
|
||||||
a
|
a, .osef
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
color: $noneColor
|
color: $noneColor
|
||||||
|
|
||||||
|
@ -294,10 +294,13 @@ input, select, button
|
||||||
.colored
|
.colored
|
||||||
color: orange
|
color: orange
|
||||||
|
|
||||||
|
.danger
|
||||||
|
color: #fdd
|
||||||
|
|
||||||
#errors
|
#errors
|
||||||
position: absolute
|
position: absolute
|
||||||
.error
|
.error
|
||||||
@include tile
|
@include main-tile
|
||||||
|
|
||||||
#content
|
#content
|
||||||
display: flex
|
display: flex
|
||||||
|
@ -310,7 +313,7 @@ input, select, button
|
||||||
|
|
||||||
#manager
|
#manager
|
||||||
background-color: $tileColor
|
background-color: $tileColor
|
||||||
border-radius: $borderRadius
|
@include rounded
|
||||||
padding-left: 1em
|
padding-left: 1em
|
||||||
height: 1.3em
|
height: 1.3em
|
||||||
display: flex
|
display: flex
|
||||||
|
@ -335,7 +338,7 @@ input, select, button
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
.service-header
|
.service-header
|
||||||
.title, .settings
|
.title, .settings
|
||||||
@include tile
|
@include main-tile
|
||||||
.title
|
.title
|
||||||
font-size: large
|
font-size: large
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
|
@ -3,13 +3,31 @@ $borderRadius: .3em
|
||||||
|
|
||||||
$backColor: #333
|
$backColor: #333
|
||||||
$tileColor: #222
|
$tileColor: #222
|
||||||
$darkColor: #111
|
$darkColor: #151515
|
||||||
$halfColor: #999
|
$halfColor: #999
|
||||||
$noneColor: #aaa
|
$noneColor: #aaa
|
||||||
$foreColor: #eee
|
$foreColor: #eee
|
||||||
|
|
||||||
|
@mixin rounded
|
||||||
|
border-radius: $borderRadius
|
||||||
|
|
||||||
@mixin tile
|
@mixin tile
|
||||||
|
@include rounded
|
||||||
margin: $borderRadius
|
margin: $borderRadius
|
||||||
background-color: $tileColor
|
background-color: $tileColor
|
||||||
border-radius: $borderRadius
|
|
||||||
padding: $borderRadius
|
@mixin main-tile
|
||||||
|
@include tile
|
||||||
|
padding: $borderRadius
|
||||||
|
|
||||||
|
@mixin group-tile
|
||||||
|
@include main-tile
|
||||||
|
padding: $borderRadius / 2
|
||||||
|
& > div
|
||||||
|
@include rounded
|
||||||
|
padding: $borderRadius
|
||||||
|
margin: $borderRadius / 2
|
||||||
|
background-color: $darkColor
|
||||||
|
border: 1px solid $backColor
|
||||||
|
&.selected
|
||||||
|
border-color: $halfColor
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
import ErrorLoadable from '@/helpers/loadable/ErrorLoadable'
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Loadable<T, E> extends Vue {
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
readonly loadable!: ErrorLoadable<T, E>
|
||||||
|
|
||||||
|
get get() {
|
||||||
|
return this.loadable.get()
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,18 +7,10 @@ div.loadable-block
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
import { Component } from 'vue-property-decorator'
|
||||||
|
|
||||||
import ErrorLoadable from '../../helpers/loadable/ErrorLoadable'
|
import Loadable from './Loadable'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class LoadableBlock<T, E> extends Vue {
|
export default class LoadableBlock<T, E> extends Loadable<T, E> { }
|
||||||
|
|
||||||
@Prop()
|
|
||||||
readonly loadable!: ErrorLoadable<T, E>
|
|
||||||
|
|
||||||
get get() {
|
|
||||||
return this.loadable.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -6,18 +6,10 @@ span.loadable-inline
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
import { Component } from 'vue-property-decorator'
|
||||||
|
|
||||||
import ErrorLoadable from '../../helpers/loadable/ErrorLoadable'
|
import Loadable from './Loadable'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class LoadableInline<T, E> extends Vue {
|
export default class LoadableInline<T, E> extends Loadable<T, E> { }
|
||||||
|
|
||||||
@Prop()
|
|
||||||
readonly loadable!: ErrorLoadable<T, E>
|
|
||||||
|
|
||||||
get get() {
|
|
||||||
return this.loadable.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -26,4 +26,10 @@ export class Selectable<T> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
with(call: (selected: T) => void) {
|
||||||
|
if (this.selected) {
|
||||||
|
call(this.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -9,7 +9,7 @@ export default class AxiosLoadable<T, E> extends ErrorLoadable<T, E> {
|
||||||
this.reset()
|
this.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
promise
|
return promise
|
||||||
.then(res => this.success(then(res)))
|
.then(res => this.success(then(res)))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
this.fail(err)
|
this.fail(err)
|
||||||
|
|
|
@ -16,11 +16,28 @@ export default class AxiosLoadableMore<T, E> extends AxiosLoadable<T, E> {
|
||||||
|
|
||||||
loadMore<U>(promise: AxiosPromise<U>, then: (res: AxiosResponse<U>, data: T) => void) {
|
loadMore<U>(promise: AxiosPromise<U>, then: (res: AxiosResponse<U>, data: T) => void) {
|
||||||
this.loadingMore = true
|
this.loadingMore = true
|
||||||
promise.then(res => {
|
return promise.then(res => {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
then(res, this.data)
|
then(res, this.data)
|
||||||
}
|
}
|
||||||
this.loadingMore = false
|
this.loadingMore = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleScroll<U>(target: ScrollTarget, promiser: (data: T) => AxiosPromise<U>, then: (res: AxiosResponse<U>, data: T) => void,
|
||||||
|
reseter?: (data: T) => void, bottom: number = 100, top: number = 20) {
|
||||||
|
if (this.data && this.loaded) {
|
||||||
|
if (!this.isLoadingMore && target.scrollHeight - target.clientHeight - target.scrollTop - bottom < 0) {
|
||||||
|
this.loadMore(promiser(this.data), then)
|
||||||
|
} else if (reseter && target.scrollTop < top) {
|
||||||
|
reseter(this.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScrollTarget {
|
||||||
|
scrollHeight: number
|
||||||
|
clientHeight: number
|
||||||
|
scrollTop: number
|
||||||
}
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template lang="pug">
|
||||||
|
.channel(:class="{ danger: channel.nsfw }") {{ name }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
import ShowMediaMixin from '@/components/ShowMediaMixin'
|
||||||
|
import { MappedChannel } from './Types'
|
||||||
|
|
||||||
|
const MAX_LENGTH = 20
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class ChannelTag extends ShowMediaMixin {
|
||||||
|
|
||||||
|
@Prop(Object)
|
||||||
|
readonly channel!: MappedChannel
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return (this.channel.parent && this.channel.parent.name ? this.channel.parent.name + ' / ' : '')
|
||||||
|
+ (this.channel.name || this.channel.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,26 +1,42 @@
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
.client(@scroll.passive="onScroll")
|
.client(@scroll.passive="onScroll")
|
||||||
loadable-block.list(:loadable="guilds")
|
loadable-block.guilds(:loadable="guilds")
|
||||||
template(#success)
|
template(#success)
|
||||||
guild(v-for="guild in guilds.get().data" :key="guild.id" :guild="guild" :showMedia="options.showMedia")
|
guild(v-for="(guild, key) in guilds.get().data" :key="guild.id" :guild="guild" :showMedia="options.showMedia"
|
||||||
|
@click.native="selectGuild(key)" :class="{ selected: guilds.get().isSelected(key) }")
|
||||||
|
loadable-block.channels(:loadable="channels")
|
||||||
|
template(#success)
|
||||||
|
channel(v-for="(channel, key) in mapChannels" :key="channel.id" :channel="channel" :showMedia="options.showMedia"
|
||||||
|
@click.native="selectChannel(key)" :class="{ selected: channels.get().isSelected(key) }")
|
||||||
|
loadable-block.messages(:loadable="messages")
|
||||||
|
template(#success)
|
||||||
|
message(v-for="(message, key) in messages.get()" :key="message.id"
|
||||||
|
:message="message" :showMedia="options.showMedia" :now="now")
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios, { AxiosResponse } from 'axios'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
import { Component, Mixins } from 'vue-property-decorator'
|
import { Component, Mixins } from 'vue-property-decorator'
|
||||||
|
|
||||||
import LoadableBlockVue from '../../components/loadable/LoadableBlock.vue'
|
import LoadableBlockVue from '@/components/loadable/LoadableBlock.vue'
|
||||||
import ServiceClient from '../../components/ServiceClient'
|
import ServiceClient from '@/components/ServiceClient'
|
||||||
import TimerMixin from '../../components/time/TimerMixin'
|
import TimerMixin from '@/components/time/TimerMixin'
|
||||||
import { Selectable } from '../../helpers/lists/Selectable'
|
import Lists from '@/helpers/lists/Lists'
|
||||||
import AxiosLoadable from '../../helpers/loadable/AxiosLoadable'
|
import { Selectable } from '@/helpers/lists/Selectable'
|
||||||
|
import AxiosLoadable from '@/helpers/loadable/AxiosLoadable'
|
||||||
|
import AxiosLoadableMore from '@/helpers/loadable/AxiosLoadableMore'
|
||||||
|
import ChannelTagVue from './ChannelTag.vue'
|
||||||
import { AUTH, getRest } from './Discord.vue'
|
import { AUTH, getRest } from './Discord.vue'
|
||||||
import GuildVue from './Guild.vue'
|
import GuildTagVue from './GuildTag.vue'
|
||||||
import { Guild, Options } from './Types'
|
import MessageTagVue from './MessageTag.vue'
|
||||||
|
import { Channel, getChannelOrder, MappedChannel, Message, Options, PartialGuild, TextChannelTypes } from './Types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
guild: GuildVue,
|
channel: ChannelTagVue,
|
||||||
|
guild: GuildTagVue,
|
||||||
|
message: MessageTagVue,
|
||||||
loadableBlock: LoadableBlockVue
|
loadableBlock: LoadableBlockVue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -28,17 +44,93 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
|
|
||||||
rest = getRest(this.auth, this.options.timeout)
|
rest = getRest(this.auth, this.options.timeout)
|
||||||
|
|
||||||
guilds = new AxiosLoadable<Selectable<Guild>, object>()
|
guilds = new AxiosLoadable<Selectable<PartialGuild>, object>()
|
||||||
|
channels = new AxiosLoadable<Selectable<Channel>, object>()
|
||||||
|
messages = new AxiosLoadableMore<Message[], object>()
|
||||||
|
|
||||||
|
get mapChannels() {
|
||||||
|
return this.channels.map(cs => cs.data
|
||||||
|
.filter(c => c.type in TextChannelTypes)
|
||||||
|
.map<MappedChannel>(c => ({ ...c,
|
||||||
|
parent: c.parent_id ? this.channels.map(pcs => pcs.data.find(pc => pc.id === c.parent_id), undefined) : undefined
|
||||||
|
})), [])
|
||||||
|
.sort(getChannelOrder)
|
||||||
|
}
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.guilds.load(
|
this.guilds.load(
|
||||||
this.get('/users/@me/guilds'),
|
this.get('/users/@me/guilds'),
|
||||||
res => new Selectable(res.data))
|
res => new Selectable(res.data)
|
||||||
|
).then(this.loadChannels)
|
||||||
|
|
||||||
|
// TODO: websocket
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChannels() {
|
||||||
|
this.channels.reset()
|
||||||
|
this.guilds.with(gs => gs.with(
|
||||||
|
guild => this.channels.load(
|
||||||
|
this.get(`/guilds/${guild.id}/channels`),
|
||||||
|
res => new Selectable(res.data)
|
||||||
|
).then(this.loadMessages)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMessages() {
|
||||||
|
this.messages.reset()
|
||||||
|
this.channels.with(cs => cs.with(
|
||||||
|
channel => this.messages.load(
|
||||||
|
this.getMessages(channel.id)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
selectGuild(id: number) {
|
||||||
|
this.guilds.with(g => g.select(id))
|
||||||
|
this.loadChannels()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectChannel(id: number) {
|
||||||
|
this.channels.with(c => c.select(id))
|
||||||
|
this.loadMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
get(path: string, options = {}) {
|
get(path: string, options = {}) {
|
||||||
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
|
return this.catchEmit(this.rest.get(path, { params: { limit: this.options.buffer, ...options } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMessages(channel: string, options = {}) {
|
||||||
|
return this.get(`/channels/${channel}/messages`, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll(event: any) {
|
||||||
|
this.channels.with(cs => cs.with(
|
||||||
|
channel => this.messages.handleScroll(event.target,
|
||||||
|
st => this.getMessages(channel.id, { before: Lists.last(st).id }),
|
||||||
|
(res, st) => Lists.pushAll(st, res.data),
|
||||||
|
st => Lists.removeFrom(st, this.options.buffer)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import '@/common.sass'
|
||||||
|
|
||||||
|
.discord
|
||||||
|
.client
|
||||||
|
height: 100%
|
||||||
|
overflow-y: auto
|
||||||
|
.guilds, .channels
|
||||||
|
@include group-tile
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: space-between
|
||||||
|
& > div
|
||||||
|
text-align: center
|
||||||
|
flex-grow: 1
|
||||||
|
.messages
|
||||||
|
@include group-tile
|
||||||
|
</style>
|
|
@ -22,11 +22,11 @@
|
||||||
import axios, { AxiosResponse } from 'axios'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
import { Component, Mixins } from 'vue-property-decorator'
|
import { Component, Mixins } from 'vue-property-decorator'
|
||||||
|
|
||||||
import AccountService from '../../components/service/AccountService';
|
import AccountService from '@/components/service/AccountService';
|
||||||
import ServiceHeaderVue from '../../components/ServiceHeader.vue'
|
import ServiceHeaderVue from '@/components/ServiceHeader.vue'
|
||||||
import { Auth } from '../../types/App'
|
import { Auth } from '@/types/App'
|
||||||
import ClientVue from './Client.vue'
|
import ClientVue from './Client.vue'
|
||||||
import { Account, Options } from './Types'
|
import { Options, User } from './Types'
|
||||||
|
|
||||||
export const AUTH = { TOKEN: 'token' }
|
export const AUTH = { TOKEN: 'token' }
|
||||||
export const CDN = 'https://cdn.discordapp.com'
|
export const CDN = 'https://cdn.discordapp.com'
|
||||||
|
@ -43,7 +43,7 @@ export function getRest(auth: Auth, timeout: number) {
|
||||||
'service-header': ServiceHeaderVue
|
'service-header': ServiceHeaderVue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export default class Discord extends Mixins<AccountService<string, object, Account>>(AccountService) { // TODO: Use oauth
|
export default class Discord extends Mixins<AccountService<string, object, User>>(AccountService) { // TODO: Use oauth
|
||||||
|
|
||||||
get params(): Options {
|
get params(): Options {
|
||||||
return { timeout: 5000, reconnect: false, buffer: 20, showMedia: true, ...this.options }
|
return { timeout: 5000, reconnect: false, buffer: 20, showMedia: true, ...this.options }
|
||||||
|
@ -61,7 +61,7 @@ export default class Discord extends Mixins<AccountService<string, object, Accou
|
||||||
return getRest(auth, this.params.timeout).get('/users/@me')
|
return getRest(auth, this.params.timeout).get('/users/@me')
|
||||||
}
|
}
|
||||||
|
|
||||||
mapAccount(res: AxiosResponse<Account>) {
|
mapAccount(res: AxiosResponse<User>) {
|
||||||
return res.data.username
|
return res.data.username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<template lang="pug">
|
|
||||||
.guild
|
|
||||||
| {{ guild.name }}
|
|
||||||
img(v-if="showMedia && guild.icon" :src="`${CDN}/icons/${guild.id}/${guild.icon}.png?size=16`")
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
|
||||||
|
|
||||||
import ShowMediaMixin from '../../components/ShowMediaMixin'
|
|
||||||
import { CDN } from './Discord.vue'
|
|
||||||
import { Guild as IGuild } from './Types'
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class Guild extends ShowMediaMixin {
|
|
||||||
|
|
||||||
@Prop(Object)
|
|
||||||
readonly guild!: IGuild
|
|
||||||
|
|
||||||
get CDN() {
|
|
||||||
return CDN
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template lang="pug">
|
||||||
|
.guild
|
||||||
|
| {{ name }}
|
||||||
|
img(v-if="iconShow" :src="iconSrc")
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
import ShowMediaMixin from '@/components/ShowMediaMixin'
|
||||||
|
import { CDN } from './Discord.vue'
|
||||||
|
import { PartialGuild } from './Types'
|
||||||
|
|
||||||
|
const MAX_LENGTH = 20
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class GuildTag extends ShowMediaMixin {
|
||||||
|
|
||||||
|
@Prop(Object)
|
||||||
|
readonly guild!: PartialGuild
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
type: Number,
|
||||||
|
default: 16
|
||||||
|
})
|
||||||
|
readonly size!: number
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
const n = this.guild.name
|
||||||
|
return n.length > MAX_LENGTH ? n.substr(0, MAX_LENGTH) + '…' : n
|
||||||
|
}
|
||||||
|
|
||||||
|
get iconShow() {
|
||||||
|
return this.showMedia && this.guild.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
get iconSrc() {
|
||||||
|
return `${CDN}/icons/${this.guild.id}/${this.guild.icon}.png?size=${this.size}`
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<template lang="pug">
|
||||||
|
.message
|
||||||
|
span.account {{ message.author.username }}
|
||||||
|
from-now.date.osef(:date="message.timestamp" :now="now")
|
||||||
|
.content(v-html="content")
|
||||||
|
a.embed(v-if="message.embeds" v-for="embed in message.embeds" :href="embed.url") {{ embed.title }}
|
||||||
|
.react(v-if="message.reactions" v-for="react in message.reactions" :class="{ colored: react.me }")
|
||||||
|
| {{ react.count }}x{{ react.emoji.name }}
|
||||||
|
//emoji(:emoji="react.emoji")
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
import ShowMediaMixin from '@/components/ShowMediaMixin'
|
||||||
|
import fromNow from '@/components/time/FromNow.vue'
|
||||||
|
import TimedMixin from '@/components/time/TimedMixin'
|
||||||
|
import { Message } from './Types'
|
||||||
|
|
||||||
|
const MAX_LENGTH = 20
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components:{
|
||||||
|
fromNow
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class MessageTag extends Mixins(ShowMediaMixin, TimedMixin) {
|
||||||
|
|
||||||
|
@Prop(Object)
|
||||||
|
readonly message!: Message
|
||||||
|
|
||||||
|
get content() {
|
||||||
|
let text = this.message.content.split('\n').join('<br />')
|
||||||
|
for (const mention of this.message.mentions) {
|
||||||
|
text = text.split(`<@${mention.id}>`).join(`<span.osef>@${mention.username}</span>`)
|
||||||
|
text = text.split(`<@!${mention.id}>`).join(`<span.osef>@${mention.username}</span>`)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.message
|
||||||
|
.date
|
||||||
|
float: right
|
||||||
|
.content
|
||||||
|
margin: .5em
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,11 +1,115 @@
|
||||||
export interface Account {
|
export type snowflake = string
|
||||||
username: string
|
interface Entity {
|
||||||
|
id: snowflake
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Guild {
|
export interface User extends Entity {
|
||||||
id: string
|
username: string
|
||||||
|
discriminator: string
|
||||||
|
avatar?: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartialGuild extends Entity {
|
||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
|
owner: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChannelType {
|
||||||
|
GUILD_TEXT = 0,
|
||||||
|
DM = 1,
|
||||||
|
GUILD_VOICE = 2,
|
||||||
|
GROUP_DM = 3,
|
||||||
|
GUILD_CATEGORY = 4,
|
||||||
|
GUILD_NEWS = 5,
|
||||||
|
GUILD_STORE = 6
|
||||||
|
}
|
||||||
|
export const TextChannelTypes = [ChannelType.DM, ChannelType.GROUP_DM, ChannelType.GUILD_TEXT]
|
||||||
|
|
||||||
|
export interface Channel extends Entity {
|
||||||
|
type: ChannelType
|
||||||
|
guild_id?: snowflake
|
||||||
|
name?: string
|
||||||
|
topic?: string
|
||||||
|
nsfw?: boolean
|
||||||
|
last_message_id?: snowflake
|
||||||
|
recipients? :User[]
|
||||||
|
icon?: string
|
||||||
|
owner_id?: snowflake
|
||||||
|
position: number
|
||||||
|
parent_id?: snowflake
|
||||||
|
}
|
||||||
|
export function getChannelOrder(a: MappedChannel, b: MappedChannel) {
|
||||||
|
function getPosition(c: MappedChannel) {
|
||||||
|
return (c.parent ? c.parent.position : 0) * 100 + c.position
|
||||||
|
}
|
||||||
|
return getPosition(a) - getPosition(b)
|
||||||
|
}
|
||||||
|
export interface MappedChannel extends Channel {
|
||||||
|
parent?: Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuildMember {
|
||||||
|
user: User
|
||||||
|
nick?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MessageType {
|
||||||
|
DEFAULT = 0,
|
||||||
|
RECIPIENT_ADD = 1,
|
||||||
|
RECIPIENT_REMOVE = 2,
|
||||||
|
CALL = 3,
|
||||||
|
CHANNEL_NAME_CHANGE = 4,
|
||||||
|
CHANNEL_ICON_CHANGE = 5,
|
||||||
|
CHANNEL_PINNED_MESSAGE = 6,
|
||||||
|
GUILD_MEMBER_JOIN = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Message extends Entity {
|
||||||
|
channel_id: snowflake
|
||||||
|
guild_id?: snowflake
|
||||||
|
author: User
|
||||||
|
member?: GuildMember
|
||||||
|
content: string
|
||||||
|
timestamp: string
|
||||||
|
edited_timestamp?: string
|
||||||
|
tts: boolean
|
||||||
|
mention_everyone: boolean
|
||||||
|
mentions: User[]
|
||||||
|
mention_roles: Role[]
|
||||||
|
attachments: Attachment[]
|
||||||
|
reactions?: Reaction[]
|
||||||
|
pinned: boolean
|
||||||
|
webhook_id?: snowflake
|
||||||
|
type: MessageType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Role extends Entity {
|
||||||
|
name: string
|
||||||
|
color: number // hexa
|
||||||
|
hoist: boolean
|
||||||
|
mentionable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Attachment extends Entity {
|
||||||
|
filename: string
|
||||||
|
size: string // bytes
|
||||||
|
url: string
|
||||||
|
proxy_url: string
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Reaction {
|
||||||
|
count: number
|
||||||
|
me: boolean
|
||||||
|
emoji: ReactionEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReactionEmoji {
|
||||||
|
id?: snowflake
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
|
|
|
@ -8,7 +8,7 @@ a.account(target="_blank" :href="account.url")
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||||
|
|
||||||
import ShowMediaMixin from '../../components/ShowMediaMixin'
|
import ShowMediaMixin from '@/components/ShowMediaMixin'
|
||||||
import { ParseEmojisMixin } from './ParseEmojisMixin'
|
import { ParseEmojisMixin } from './ParseEmojisMixin'
|
||||||
import { Account as IAccount } from './Types'
|
import { Account as IAccount } from './Types'
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,12 @@
|
||||||
import axios, { AxiosResponse } from 'axios'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
import { Component, Mixins } from 'vue-property-decorator'
|
import { Component, Mixins } from 'vue-property-decorator'
|
||||||
|
|
||||||
import LoadableBlockVue from '../../components/loadable/LoadableBlock.vue'
|
import LoadableBlockVue from '@/components/loadable/LoadableBlock.vue'
|
||||||
import ServiceClient from '../../components/ServiceClient'
|
import ServiceClient from '@/components/ServiceClient'
|
||||||
import TimerMixin from '../../components/time/TimerMixin'
|
import TimerMixin from '@/components/time/TimerMixin'
|
||||||
import Lists from '../../helpers/lists/Lists'
|
import Lists from '@/helpers/lists/Lists'
|
||||||
import AxiosLodable from '../../helpers/loadable/AxiosLoadable'
|
import AxiosLodable from '@/helpers/loadable/AxiosLoadable'
|
||||||
import AxiosLodableMore from '../../helpers/loadable/AxiosLoadableMore'
|
import AxiosLodableMore from '@/helpers/loadable/AxiosLoadableMore'
|
||||||
import { AUTH, getRest } from './Mastodon.vue'
|
import { AUTH, getRest } from './Mastodon.vue'
|
||||||
import NotificationVue from './Notification.vue'
|
import NotificationVue from './Notification.vue'
|
||||||
import StatusVue from './Status.vue'
|
import StatusVue from './Status.vue'
|
||||||
|
@ -78,14 +78,11 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll(event: any) {
|
onScroll(event: any) {
|
||||||
if(!this.statues.isLoadingMore && event.target.scrollHeight - event.target.clientHeight - event.target.scrollTop - 100 < 0) {
|
this.statues.handleScroll(event.target,
|
||||||
this.statues.loadMore(
|
st => this.getTimeline({ max_id: Lists.last(st).id }),
|
||||||
this.getTimeline({ max_id: this.statues.map(s => Lists.last(s).id , 0) }),
|
(res, st) => Lists.pushAll(st, res.data),
|
||||||
(res, statues) => Lists.pushAll(statues, res.data)
|
st => Lists.removeFrom(st, this.options.buffer)
|
||||||
)
|
)
|
||||||
} else if(event.target.scrollTop < 20) {
|
|
||||||
this.statues.with(s => Lists.removeFrom(s, this.options.buffer))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showStatus(status: Status) {
|
showStatus(status: Status) {
|
||||||
|
@ -146,7 +143,7 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
|
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@import '../../common.sass'
|
@import '@/common.sass'
|
||||||
|
|
||||||
.mastodon
|
.mastodon
|
||||||
.client
|
.client
|
||||||
|
@ -154,10 +151,9 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
height: 100%
|
height: 100%
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
.header
|
.header
|
||||||
@include tile
|
@include main-tile
|
||||||
.list
|
.list
|
||||||
& > div
|
@include group-tile
|
||||||
@include tile
|
|
||||||
.statues
|
.statues
|
||||||
flex: 1
|
flex: 1
|
||||||
.notifications
|
.notifications
|
||||||
|
@ -169,7 +165,7 @@ export default class Client extends Mixins<ServiceClient<Options>>(ServiceClient
|
||||||
color: $foreColor
|
color: $foreColor
|
||||||
.avatar
|
.avatar
|
||||||
float: left
|
float: left
|
||||||
border-radius: $borderRadius
|
@include rounded
|
||||||
width: $avatarSize
|
width: $avatarSize
|
||||||
height: $avatarSize
|
height: $avatarSize
|
||||||
background-size: $avatarSize $avatarSize
|
background-size: $avatarSize $avatarSize
|
||||||
|
|
|
@ -31,9 +31,9 @@
|
||||||
import axios, { AxiosResponse } from 'axios'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
import { Component, Mixins } from 'vue-property-decorator'
|
import { Component, Mixins } from 'vue-property-decorator'
|
||||||
|
|
||||||
import AccountService from '../../components/service/AccountService'
|
import AccountService from '@/components/service/AccountService'
|
||||||
import ServiceHeaderVue from '../../components/ServiceHeader.vue'
|
import ServiceHeaderVue from '@/components/ServiceHeader.vue'
|
||||||
import { Auth } from '../../types/App'
|
import { Auth } from '@/types/App'
|
||||||
import ClientVue from './Client.vue'
|
import ClientVue from './Client.vue'
|
||||||
import { ParseEmojisMixin } from './ParseEmojisMixin'
|
import { ParseEmojisMixin } from './ParseEmojisMixin'
|
||||||
import { Account, Options } from './Types'
|
import { Account, Options } from './Types'
|
||||||
|
|
|
@ -19,9 +19,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator'
|
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator'
|
||||||
|
|
||||||
import ShowMediaMixin from '../../components/ShowMediaMixin'
|
import ShowMediaMixin from '@/components/ShowMediaMixin'
|
||||||
import FromNowVue from '../../components/time/FromNow.vue'
|
import FromNowVue from '@/components/time/FromNow.vue'
|
||||||
import TimedMixin from '../../components/time/TimedMixin'
|
import TimedMixin from '@/components/time/TimedMixin'
|
||||||
import AccountVue from './Account.vue'
|
import AccountVue from './Account.vue'
|
||||||
import StatusVue from './Status.vue'
|
import StatusVue from './Status.vue'
|
||||||
import { MarkMessage, Notification as INotification } from './Types'
|
import { MarkMessage, Notification as INotification } from './Types'
|
||||||
|
|
|
@ -38,9 +38,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator'
|
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator'
|
||||||
|
|
||||||
import ShowMediaMixin from '../../components/ShowMediaMixin'
|
import ShowMediaMixin from '@/components/ShowMediaMixin'
|
||||||
import FromNowVue from '../../components/time/FromNow.vue'
|
import FromNowVue from '@/components/time/FromNow.vue'
|
||||||
import TimedMixin from '../../components/time/TimedMixin'
|
import TimedMixin from '@/components/time/TimedMixin'
|
||||||
import AccountVue from './Account.vue'
|
import AccountVue from './Account.vue'
|
||||||
import { ParseEmojisMixin } from './ParseEmojisMixin'
|
import { ParseEmojisMixin } from './ParseEmojisMixin'
|
||||||
import { MarkMessage, Status as IStatus } from './Types'
|
import { MarkMessage, Status as IStatus } from './Types'
|
||||||
|
|
|
@ -35,13 +35,13 @@
|
||||||
import axios, { AxiosInstance, AxiosResponse } from 'axios'
|
import axios, { AxiosInstance, AxiosResponse } from 'axios'
|
||||||
import { Component, Mixins } from 'vue-property-decorator'
|
import { Component, Mixins } from 'vue-property-decorator'
|
||||||
|
|
||||||
import ConnectedService from '../../components/service/ConnectedService'
|
import ConnectedService from '@/components/service/ConnectedService'
|
||||||
import ServiceHeaderVue from '../../components/ServiceHeader.vue'
|
import ServiceHeaderVue from '@/components/ServiceHeader.vue'
|
||||||
import FromNowVue from '../../components/time/FromNow.vue'
|
import FromNowVue from '@/components/time/FromNow.vue'
|
||||||
import TimerMixin from '../../components/time/TimerMixin'
|
import TimerMixin from '@/components/time/TimerMixin'
|
||||||
import Lists from '../../helpers/lists/Lists'
|
import Lists from '@/helpers/lists/Lists'
|
||||||
import AxiosLoadable from '../../helpers/loadable/AxiosLoadable'
|
import AxiosLoadable from '@/helpers/loadable/AxiosLoadable'
|
||||||
import { Auth } from '../../types/App'
|
import { Auth } from '@/types/App'
|
||||||
|
|
||||||
interface News {
|
interface News {
|
||||||
id: number
|
id: number
|
||||||
|
@ -144,17 +144,17 @@ export default class NextcloudNews extends Mixins<ConnectedService<object, objec
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@import '../../common.sass'
|
@import '@/common.sass'
|
||||||
|
|
||||||
.nextcloud-news
|
.nextcloud-news
|
||||||
.unreaded
|
.unreaded
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
.news
|
@include group-tile
|
||||||
@include tile
|
.news
|
||||||
.date
|
.date
|
||||||
float: right
|
float: right
|
||||||
.read
|
.read
|
||||||
margin-right: .5em
|
margin-right: .5em
|
||||||
.content
|
.content
|
||||||
padding: $borderRadius
|
padding: $borderRadius
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
loadable-block(:loadable="weathers")
|
loadable-block(:loadable="weathers")
|
||||||
template(#success)
|
template(#success)
|
||||||
.list
|
.list
|
||||||
weather(v-for="(city, id) in weathers.get().data" :key="id" :selected="weathers.get().selectedId == id"
|
weather(v-for="(city, id) in weathers.get().data" :key="id" :class="{ selected: weathers.get().isSelected(id) }"
|
||||||
:city="city" @select="makeSelect(id)" @remove="removeCity(id)")
|
:city="city" @select="makeSelect(id)" @remove="removeCity(id)")
|
||||||
input.weather(v-show="showAdd" placeholder="city id" @keyup.enter="addCity(parseInt($event.target.value))")
|
input.weather(v-show="showAdd" placeholder="city id" @keyup.enter="addCity(parseInt($event.target.value))")
|
||||||
loadable-block(:loadable="forecast").forecast
|
loadable-block.forecast(:loadable="forecast")
|
||||||
template(#success)
|
template(#success)
|
||||||
chart.chart(:chartData="forecastChart")
|
chart.chart(:chartData="forecastChart")
|
||||||
template(#error)
|
template(#error)
|
||||||
|
@ -29,12 +29,12 @@
|
||||||
import axios, { AxiosInstance, AxiosResponse } from 'axios'
|
import axios, { AxiosInstance, AxiosResponse } from 'axios'
|
||||||
import { Component } from 'vue-property-decorator'
|
import { Component } from 'vue-property-decorator'
|
||||||
|
|
||||||
import ConnectedService from '../../components/service/ConnectedService'
|
import ConnectedService from '@/components/service/ConnectedService'
|
||||||
import ServiceHeaderVue from '../../components/ServiceHeader.vue'
|
import ServiceHeaderVue from '@/components/ServiceHeader.vue'
|
||||||
import Lists from '../../helpers/lists/Lists'
|
import Lists from '@/helpers/lists/Lists'
|
||||||
import { Selectable } from '../../helpers/lists/Selectable'
|
import { Selectable } from '@/helpers/lists/Selectable'
|
||||||
import AxiosLoadable from '../../helpers/loadable/AxiosLoadable'
|
import AxiosLoadable from '@/helpers/loadable/AxiosLoadable'
|
||||||
import { Auth } from '../../types/App'
|
import { Auth } from '@/types/App'
|
||||||
import Chart from './Chart'
|
import Chart from './Chart'
|
||||||
import WeatherVue, { IWeather } from './Weather.vue'
|
import WeatherVue, { IWeather } from './Weather.vue'
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ export default class OpenWeatherMap extends ConnectedService<object, object> {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@import '../../common.sass'
|
@import '@/common.sass'
|
||||||
|
|
||||||
.openweathermap
|
.openweathermap
|
||||||
.loadable-block
|
.loadable-block
|
||||||
|
@ -218,10 +218,11 @@ export default class OpenWeatherMap extends ConnectedService<object, object> {
|
||||||
.list
|
.list
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
.weather, .forecast
|
@include group-tile
|
||||||
|
.weather
|
||||||
flex: 1
|
flex: 1
|
||||||
@include tile
|
|
||||||
.forecast
|
.forecast
|
||||||
|
@include main-tile
|
||||||
flex: 1
|
flex: 1
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
height: 100%
|
height: 100%
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
.weather(:class="{ selected }" @click.stop.prevent="$emit('select')")
|
.weather(@click.stop.prevent="$emit('select')")
|
||||||
.main(v-for="main in city.weather")
|
.main(v-for="main in city.weather")
|
||||||
p {{ main.description }}
|
p {{ main.description }}
|
||||||
.ic
|
.ic
|
||||||
|
@ -38,14 +38,11 @@ export default class Weather extends Vue {
|
||||||
@Prop(Object)
|
@Prop(Object)
|
||||||
readonly city!: IWeather
|
readonly city!: IWeather
|
||||||
|
|
||||||
@Prop(Boolean)
|
|
||||||
readonly selected!: boolean
|
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@import '../../common.sass'
|
@import '@/common.sass'
|
||||||
|
|
||||||
.weather
|
.weather
|
||||||
min-width: 17em
|
min-width: 17em
|
||||||
|
@ -54,8 +51,6 @@ export default class Weather extends Vue {
|
||||||
grid-template-columns: auto auto
|
grid-template-columns: auto auto
|
||||||
grid-template-rows: 1.2em auto
|
grid-template-rows: 1.2em auto
|
||||||
grid-template-areas: "header main" "data remove"
|
grid-template-areas: "header main" "data remove"
|
||||||
&.selected
|
|
||||||
border-color: $halfColor
|
|
||||||
.header
|
.header
|
||||||
grid-area: header
|
grid-area: header
|
||||||
font-size: 1.2em
|
font-size: 1.2em
|
||||||
|
|
Loading…
Reference in New Issue