1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-14 13:25:48 +02:00

Merge branch 'develop' into vue3

This commit is contained in:
syuilo
2020-07-30 10:34:39 +09:00
101 changed files with 1671 additions and 370 deletions

View File

@@ -4,7 +4,7 @@
<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
<x-timeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
</x-column>
</template>
@@ -33,7 +33,6 @@ export default defineComponent({
data() {
return {
menu: null,
faSatellite
};
},
@@ -47,28 +46,36 @@ export default defineComponent({
created() {
this.menu = [{
icon: faCog,
text: this.$t('antenna'),
action: async () => {
const antennas = await this.$root.api('antennas/list');
this.$root.dialog({
title: this.$t('antenna'),
type: null,
select: {
items: antennas.map(x => ({
value: x, text: x.name
}))
},
showCancelButton: true
}).then(({ canceled, result: antenna }) => {
if (canceled) return;
this.column.antennaId = antenna.id;
this.$store.commit('deviceUser/updateDeckColumn', this.column);
});
}
text: this.$t('selectAntenna'),
action: this.setAntenna
}];
},
mounted() {
if (this.column.antennaId == null) {
this.setAntenna();
}
},
methods: {
async setAntenna() {
const antennas = await this.$root.api('antennas/list');
const { canceled, result: antenna } = await this.$root.dialog({
title: this.$t('selectAntenna'),
type: null,
select: {
items: antennas.map(x => ({
value: x, text: x.name
})),
default: this.column.antennaId
},
showCancelButton: true
});
if (canceled) return;
Vue.set(this.column, 'antennaId', antenna.id);
this.$store.commit('deviceUser/updateDeckColumn', this.column);
},
focus() {
(this.$refs.timeline as any).focus();
}

View File

@@ -150,37 +150,37 @@ export default defineComponent({
}
}, null, {
icon: faArrowLeft,
text: this.$t('swap-left'),
text: this.$t('_deck.swapLeft'),
action: () => {
this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id);
}
}, {
icon: faArrowRight,
text: this.$t('swap-right'),
text: this.$t('_deck.swapRight'),
action: () => {
this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id);
}
}, this.isStacked ? {
icon: faArrowUp,
text: this.$t('swap-up'),
text: this.$t('_deck.swapUp'),
action: () => {
this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id);
}
} : undefined, this.isStacked ? {
icon: faArrowDown,
text: this.$t('swap-down'),
text: this.$t('_deck.swapDown'),
action: () => {
this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id);
}
} : undefined, null, {
icon: faWindowRestore,
text: this.$t('stack-left'),
text: this.$t('_deck.stackLeft'),
action: () => {
this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id);
}
}, this.isStacked ? {
icon: faWindowMaximize,
text: this.$t('pop-right'),
text: this.$t('_deck.popRight'),
action: () => {
this.$store.commit('deviceUser/popRightDeckColumn', this.column.id);
}

View File

@@ -46,7 +46,7 @@ export default defineComponent({
created() {
this.menu = [{
icon: faCog,
text: this.$t('list'),
text: this.$t('selectList'),
action: this.setList
}];
},
@@ -61,7 +61,7 @@ export default defineComponent({
async setList() {
const lists = await this.$root.api('users/lists/list');
const { canceled, result: list } = await this.$root.dialog({
title: this.$t('list'),
title: this.$t('selectList'),
type: null,
select: {
items: lists.map(x => ({

View File

@@ -45,14 +45,14 @@ export default defineComponent({
this.menu = [{
icon: faCog,
text: this.$t('@.notification-type'),
text: this.$t('notificationType'),
action: () => {
this.$root.dialog({
title: this.$t('@.notification-type'),
title: this.$t('notificationType'),
type: null,
select: {
items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
value: x, text: this.$t('@.notification-types.' + x)
value: x, text: this.$t(`_notification._types.${x}`)
}))
default: this.column.notificationType,
},

View File

@@ -5,9 +5,12 @@
<div class="wtdtxvec">
<template v-if="edit">
<header>
<select v-model="widgetAdderSelected" @change="addWidget">
<option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option>
</select>
<mk-select v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
<template #label>{{ $t('selectWidget') }}</template>
<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
</mk-select>
<mk-button inline @click="addWidget" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
<mk-button inline @click="edit = false">{{ $t('close') }}</mk-button>
</header>
<x-draggable
:list="column.widgets"
@@ -15,7 +18,7 @@
@sort="onWidgetSort"
>
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)">
<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><fa :icon="faTimes"/></button>
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/>
</div>
</x-draggable>
@@ -29,7 +32,9 @@
import { defineComponent } from 'vue';
import * as XDraggable from 'vuedraggable';
import { v4 as uuid } from 'uuid';
import { faWindowMaximize, faTimes, faCog } from '@fortawesome/free-solid-svg-icons';
import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '../../components/ui/select.vue';
import MkButton from '../../components/ui/button.vue';
import XColumn from './column.vue';
import { widgets } from '../../widgets';
@@ -37,6 +42,8 @@ export default defineComponent({
components: {
XColumn,
XDraggable,
MkSelect,
MkButton,
},
props: {
@@ -56,7 +63,7 @@ export default defineComponent({
menu: null,
widgetAdderSelected: null,
widgets,
faWindowMaximize, faTimes
faWindowMaximize, faTimes, faPlus
};
},
@@ -80,6 +87,8 @@ export default defineComponent({
},
addWidget() {
if (this.widgetAdderSelected == null) return;
this.$store.commit('deviceUser/addDeckWidget', {
id: this.column.id,
widget: {

View File

@@ -5,10 +5,22 @@
</template>
<div class="xkpnjxcv">
<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"><span v-text="form[item].label || item"></span></mk-input>
<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text"><span v-text="form[item].label || item"></span></mk-input>
<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-textarea>
<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-switch>
<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</mk-input>
<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</mk-input>
<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</mk-textarea>
<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</mk-switch>
</label>
</div>
</x-window>
@@ -48,7 +60,7 @@ export default defineComponent({
created() {
for (const item in this.form) {
Vue.set(this.values, item, this.form[item].default || null);
Vue.set(this.values, item, this.form[item].hasOwnProperty('default') ? this.form[item].default : null);
}
},

View File

@@ -1,7 +1,8 @@
<template>
<div
class="note _panel"
v-show="!isDeleted && !hideThisNote"
v-if="!muted"
v-show="!isDeleted"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
v-hotkey="keymap"
@@ -34,19 +35,19 @@
</div>
</div>
<article class="article">
<mk-avatar class="avatar" :user="appearNote.user" v-once/>
<mk-avatar class="avatar" :user="appearNote.user"/>
<div class="main">
<x-note-header class="header" :note="appearNote" :mini="true"/>
<div class="body" v-if="appearNote.deletedAt == null" ref="noteBody">
<div class="body" ref="noteBody">
<p v-if="appearNote.cw != null" class="cw">
<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
<x-cw-button v-model="showContent" :note="appearNote"/>
</p>
<div class="content" v-show="appearNote.cw == null || showContent">
<div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
<a class="rp" v-if="appearNote.renote != null">RN:</a>
</div>
<div class="files" v-if="appearNote.files.length > 0">
@@ -57,7 +58,7 @@
<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
</div>
</div>
<footer v-if="appearNote.deletedAt == null" class="footer">
<footer class="footer">
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
<button @click="reply()" class="button _button">
<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
@@ -80,11 +81,17 @@
<fa :icon="faEllipsisH"/>
</button>
</footer>
<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
</div>
</article>
<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
</div>
<div v-else class="_panel muted" @click="muted = false">
<i18n-t path="userSaysSomething" tag="small">
<router-link class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId" place="name">
<mk-user-name :user="appearNote.user"/>
</router-link>
</i18n-t>
</div>
</template>
<script lang="ts">
@@ -106,9 +113,16 @@ import pleaseLogin from '../scripts/please-login';
import { focusPrev, focusNext } from '../scripts/focus';
import { url } from '../config';
import copyToClipboard from '../scripts/copy-to-clipboard';
import { checkWordMute } from '../scripts/check-word-mute';
import { utils } from '@syuilo/aiscript';
import { userPage } from '../filters/user';
export default defineComponent({
model: {
prop: 'note',
event: 'updated'
},
components: {
XSub,
XNoteHeader,
@@ -143,7 +157,8 @@ export default defineComponent({
conversation: [],
replies: [],
showContent: false,
hideThisNote: false,
isDeleted: false,
muted: false,
noteBody: this.$refs.noteBody,
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
};
@@ -187,10 +202,6 @@ export default defineComponent({
return this.isRenote ? this.note.renote : this.note;
},
isDeleted(): boolean {
return this.appearNote.deletedAt != null || this.note.deletedAt != null;
},
isMyNote(): boolean {
return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
},
@@ -232,11 +243,22 @@ export default defineComponent({
}
},
created() {
async created() {
if (this.$store.getters.isSignedIn) {
this.connection = this.$root.stream;
}
// plugin
if (this.$store.state.noteViewInterruptors.length > 0) {
let result = this.note;
for (const interruptor of this.$store.state.noteViewInterruptors) {
result = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(result))));
}
this.$emit('updated', Object.freeze(result));
}
this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
if (this.detail) {
this.$root.api('notes/children', {
noteId: this.appearNote.id,
@@ -262,7 +284,7 @@ export default defineComponent({
this.connection.on('_connected_', this.onStreamConnected);
}
this.noteBody = this.$refs.noteBody
this.noteBody = this.$refs.noteBody;
},
beforeDestroy() {
@@ -274,11 +296,24 @@ export default defineComponent({
},
methods: {
updateAppearNote(v) {
this.$emit('updated', Object.freeze(this.isRenote ? {
...this.note,
renote: {
...this.note.renote,
...v
}
} : {
...this.note,
...v
}));
},
readPromo() {
(this as any).$root.api('promo/read', {
noteId: this.appearNote.id
});
this.hideThisNote = true;
this.isDeleted = true;
},
capture(withHandler = false) {
@@ -310,67 +345,88 @@ export default defineComponent({
case 'reacted': {
const reaction = body.reaction;
// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
let n = {
...this.appearNote,
};
if (body.emoji) {
const emojis = this.appearNote.emojis || [];
if (!emojis.includes(body.emoji)) {
emojis.push(body.emoji);
Vue.set(this.appearNote, 'emojis', emojis);
n.emojis = [...emojis, body.emoji];
}
}
if (this.appearNote.reactions == null) {
Vue.set(this.appearNote, 'reactions', {});
}
if (this.appearNote.reactions[reaction] == null) {
Vue.set(this.appearNote.reactions, reaction, 0);
}
// TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる
const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
// Increment the count
this.appearNote.reactions[reaction]++;
n.reactions = {
...this.appearNote.reactions,
[reaction]: currentCount + 1
};
if (body.userId == this.$store.state.i.id) {
Vue.set(this.appearNote, 'myReaction', reaction);
if (body.userId === this.$store.state.i.id) {
n.myReaction = reaction;
}
this.updateAppearNote(n);
break;
}
case 'unreacted': {
const reaction = body.reaction;
if (this.appearNote.reactions == null) {
return;
}
// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
let n = {
...this.appearNote,
};
if (this.appearNote.reactions[reaction] == null) {
return;
}
// TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる
const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
// Decrement the count
if (this.appearNote.reactions[reaction] > 0) this.appearNote.reactions[reaction]--;
n.reactions = {
...this.appearNote.reactions,
[reaction]: Math.max(0, currentCount - 1)
};
if (body.userId == this.$store.state.i.id) {
Vue.set(this.appearNote, 'myReaction', null);
if (body.userId === this.$store.state.i.id) {
n.myReaction = null;
}
this.updateAppearNote(n);
break;
}
case 'pollVoted': {
const choice = body.choice;
this.appearNote.poll.choices[choice].votes++;
if (body.userId == this.$store.state.i.id) {
Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true);
}
// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
let n = {
...this.appearNote,
};
n.poll = {
...this.appearNote.poll,
choices: {
...this.appearNote.poll.choices,
[choice]: {
...this.appearNote.poll.choices[choice],
votes: this.appearNote.poll.choices[choice].votes + 1,
...(body.userId === this.$store.state.i.id ? {
isVoted: true
} : {})
}
}
};
this.updateAppearNote(n);
break;
}
case 'deleted': {
Vue.set(this.appearNote, 'deletedAt', body.deletedAt);
Vue.set(this.appearNote, 'renote', null);
this.appearNote.text = null;
this.appearNote.fileIds = [];
this.appearNote.poll = null;
this.appearNote.cw = null;
this.isDeleted = true;
break;
}
}
@@ -639,7 +695,7 @@ export default defineComponent({
this.$root.api('notes/delete', {
noteId: this.note.id
});
Vue.set(this.note, 'deletedAt', new Date());
this.isDeleted = true;
}
}],
source: this.$refs.renoteTime,
@@ -928,10 +984,6 @@ export default defineComponent({
}
}
}
> .deleted {
opacity: 0.7;
}
}
}
@@ -998,4 +1050,10 @@ export default defineComponent({
}
}
}
.muted {
padding: 8px;
text-align: center;
opacity: 0.7;
}
</style>

View File

@@ -15,7 +15,7 @@
</div>
<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
<x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
<x-note :note="note" @updated="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
</x-list>
<div v-show="more && !reversed" style="margin-top: var(--margin);">
@@ -62,14 +62,15 @@ export default defineComponent({
default: false
},
extract: {
prop: {
type: String,
required: false
}
},
computed: {
notes(): any[] {
return this.extract ? this.extract(this.items) : this.items;
return this.prop ? this.items.map(item => item[this.prop]) : this.items;
},
reversed(): boolean {
@@ -78,6 +79,15 @@ export default defineComponent({
},
methods: {
updated(oldValue, newValue) {
const i = this.notes.findIndex(n => n === oldValue);
if (this.prop) {
Vue.set(this.items[i], this.prop, newValue);
} else {
Vue.set(this.items, i, newValue);
}
},
focus() {
this.$refs.notes.focus();
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="mfcuwfyp">
<x-list class="notifications" :items="items" v-slot="{ item: notification }">
<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" :key="notification.id"/>
<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @updated="noteUpdated(notification.note, $event)" :key="notification.id"/>
<x-notification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
</x-list>
@@ -75,11 +75,20 @@ export default defineComponent({
this.$root.stream.send('readNotification', {
id: notification.id
});
notification.isRead = true;
}
this.prepend(notification);
this.prepend({
...notification,
isRead: document.visibilityState === 'visible'
});
},
noteUpdated(oldValue, newValue) {
const i = this.items.findIndex(n => n.note === oldValue);
Vue.set(this.items, i, {
...this.items[i],
note: newValue
});
},
}
});

View File

@@ -69,6 +69,7 @@ import getAcct from '../../misc/acct/render';
import { formatTimeString } from '../../misc/format-time-string';
import { selectDriveFile } from '../scripts/select-drive-file';
import { noteVisibilities } from '../../types';
import { utils } from '@syuilo/aiscript';
export default defineComponent({
components: {
@@ -533,9 +534,8 @@ export default defineComponent({
localStorage.setItem('drafts', JSON.stringify(data));
},
post() {
this.posting = true;
this.$root.api('notes/create', {
async post() {
let data = {
text: this.text == '' ? undefined : this.text,
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
@@ -546,7 +546,17 @@ export default defineComponent({
visibility: this.visibility,
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
viaMobile: this.$root.isMobile
}).then(data => {
};
// plugin
if (this.$store.state.notePostInterruptors.length > 0) {
for (const interruptor of this.$store.state.notePostInterruptors) {
data = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(data))));
}
}
this.posting = true;
this.$root.api('notes/create', data).then(() => {
this.clear();
this.deleteDraft();
this.$emit('posted');

View File

@@ -0,0 +1,46 @@
<template>
<div class="pxhvhrfw" v-size="[{ max: 500 }]">
<button v-for="item in items" class="_button" @click="$emit('input', item.value)" :class="{ active: value === item.value }" :key="item.value"><fa v-if="item.icon" :icon="item.icon" class="icon"/>{{ item.label }}</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
items: {
type: Array,
required: true,
},
value: {
required: true,
},
},
});
</script>
<style lang="scss" scoped>
.pxhvhrfw {
display: flex;
> button {
flex: 1;
padding: 11px 8px 8px 8px;
border-bottom: solid 3px transparent;
&.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
> .icon {
margin-right: 6px;
}
}
&.max-width_500px {
font-size: 80%;
}
}
</style>

View File

@@ -47,8 +47,7 @@ export default defineComponent({
created() {
const prepend = note => {
const _note = JSON.parse(JSON.stringify(note)); // deepcopy
(this.$refs.tl as any).prepend(_note);
(this.$refs.tl as any).prepend(note);
this.$emit('note');

View File

@@ -223,7 +223,7 @@ stream.on('emojiAdded', data => {
//store.commit('instance/set', );
});
for (const plugin of store.state.deviceUser.plugins) {
for (const plugin of store.state.deviceUser.plugins.filter(p => p.active)) {
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
const aiscript = new AiScript(createPluginEnv(app, {

View File

@@ -2,7 +2,7 @@
<div>
<portal to="icon"><fa :icon="faStar"/></portal>
<portal to="title">{{ $t('favorites') }}</portal>
<x-notes :pagination="pagination" :detail="true" :extract="items => items.map(item => item.note)" @before="before()" @after="after()"/>
<x-notes :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
</div>
</template>

View File

@@ -437,7 +437,7 @@ export default defineComponent({
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},

View File

@@ -170,7 +170,7 @@ export default defineComponent({
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},

View File

@@ -28,6 +28,9 @@
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
</div>
<div class="_content">
<mk-switch v-model="useStarForReactionFallback" @change="save()">{{ $t('useStarForReactionFallback') }}</mk-switch>
</div>
</section>
<section class="_card info">
@@ -74,6 +77,29 @@
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
<div class="_content">
<mk-switch v-model="enableEmail" @change="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></mk-switch>
<mk-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</mk-input>
<div><b>{{ $t('smtpConfig') }}</b></div>
<div class="_inputs">
<mk-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtpHost') }}</mk-input>
<mk-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtpPort') }}</mk-input>
</div>
<div class="_inputs">
<mk-input v-model="smtpUser" :disabled="!enableEmail">{{ $t('smtpUser') }}</mk-input>
<mk-input v-model="smtpPass" type="password" :disabled="!enableEmail">{{ $t('smtpPass') }}</mk-input>
</div>
<mk-info>{{ $t('emptyToDisableSmtpAuth') }}</mk-info>
<mk-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtpSecure') }}<template #desc>{{ $t('smtpSecureInfo') }}</template></mk-switch>
<div>
<mk-button :disabled="!enableEmail" inline @click="testEmail()">{{ $t('testEmail') }}</mk-button>
<mk-button :disabled="!enableEmail" primary inline @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
@@ -195,12 +221,19 @@
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faArchway" /> Summaly Proxy</div>
<div class="_content">
<mk-input v-model="summalyProxy">URL</mk-input>
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import MkButton from '../../components/ui/button.vue';
@@ -243,7 +276,9 @@ export default defineComponent({
maintainerEmail: null,
name: null,
description: null,
tosUrl: null,
tosUrl: null as string | null,
enableEmail: false,
email: null,
bannerUrl: null,
iconUrl: null,
maxNoteTextLength: 0,
@@ -279,7 +314,14 @@ export default defineComponent({
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
useStarForReactionFallback: false,
smtpSecure: false,
smtpHost: '',
smtpPort: 0,
smtpUser: '',
smtpPass: '',
summalyProxy: '',
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway
}
},
@@ -295,6 +337,8 @@ export default defineComponent({
this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl;
this.enableEmail = this.meta.enableEmail;
this.email = this.meta.email;
this.maintainerName = this.meta.maintainerName;
this.maintainerEmail = this.meta.maintainerEmail;
this.maxNoteTextLength = this.meta.maxNoteTextLength;
@@ -337,6 +381,13 @@ export default defineComponent({
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.discordClientId = this.meta.discordClientId;
this.discordClientSecret = this.meta.discordClientSecret;
this.useStarForReactionFallback = this.meta.useStarForReactionFallback;
this.smtpSecure = this.meta.smtpSecure;
this.smtpHost = this.meta.smtpHost;
this.smtpPort = this.meta.smtpPort;
this.smtpUser = this.meta.smtpUser;
this.smtpPass = this.meta.smtpPass;
this.summalyProxy = this.meta.summalyProxy;
if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
@@ -412,6 +463,24 @@ export default defineComponent({
});
},
async testEmail() {
this.$root.api('admin/send-email', {
to: this.maintainerEmail,
subject: 'Test email',
text: 'Yo'
}).then(x => {
this.$root.dialog({
type: 'success',
splash: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
},
save(withDialog = false) {
this.$root.api('admin/update-meta', {
name: this.name,
@@ -461,6 +530,15 @@ export default defineComponent({
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
enableEmail: this.enableEmail,
email: this.email,
smtpSecure: this.smtpSecure,
smtpHost: this.smtpHost,
smtpPort: this.smtpPort,
smtpUser: this.smtpUser,
smtpPass: this.smtpPass,
summalyProxy: this.summalyProxy,
useStarForReactionFallback: this.useStarForReactionFallback,
}).then(() => {
this.$store.dispatch('instance/fetch');
if (withDialog) {

View File

@@ -27,6 +27,7 @@
<x-import-export/>
<x-drive/>
<x-mute-block/>
<x-word-mute/>
<x-security/>
<x-2fa/>
<x-integration/>
@@ -47,6 +48,7 @@ import XImportExport from './import-export.vue';
import XDrive from './drive.vue';
import XReactionSetting from './reaction.vue';
import XMuteBlock from './mute-block.vue';
import XWordMute from './word-mute.vue';
import XSecurity from './security.vue';
import X2fa from './2fa.vue';
import XIntegration from './integration.vue';
@@ -68,6 +70,7 @@ export default defineComponent({
XDrive,
XReactionSetting,
XMuteBlock,
XWordMute,
XSecurity,
X2fa,
XIntegration,

View File

@@ -0,0 +1,77 @@
<template>
<section class="_card">
<div class="_title"><fa :icon="faCommentSlash"/> {{ $t('wordMute') }}</div>
<div class="_content _noPad">
<mk-tab v-model="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
</div>
<div class="_content" v-show="tab === 'soft'">
<mk-info>{{ $t('_wordMute.softDescription') }}</mk-info>
<mk-textarea v-model="softMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
</mk-textarea>
</div>
<div class="_content" v-show="tab === 'hard'">
<mk-info>{{ $t('_wordMute.hardDescription') }}</mk-info>
<mk-textarea v-model="hardMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
</template>
<script lang="ts">
import Vue from 'vue';
import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import MkTab from '../../components/tab.vue';
import MkInfo from '../../components/ui/info.vue';
export default Vue.extend({
components: {
MkButton,
MkTextarea,
MkTab,
MkInfo,
},
data() {
return {
tab: 'soft',
softMutedWords: '',
hardMutedWords: '',
changed: false,
faCommentSlash, faSave,
}
},
watch: {
softMutedWords() {
this.changed = true;
},
hardMutedWords() {
this.changed = true;
},
},
created() {
this.softMutedWords = this.$store.state.settings.mutedWords.map(x => x.join(' ')).join('\n');
this.hardMutedWords = this.$store.state.i.mutedWords.map(x => x.join(' ')).join('\n');
},
methods: {
async save() {
this.$store.dispatch('settings/set', { key: 'mutedWords', value: this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')) });
await this.$root.api('i/update', {
mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
});
this.changed = false;
},
}
});
</script>

View File

@@ -14,7 +14,7 @@
<hr v-if="showNext"/>
<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
<x-note :note="note" :key="note.id" :detail="true"/>
<x-note v-model="note" :key="note.id" :detail="true"/>
<button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button>
<hr v-if="showPrev"/>

View File

@@ -3,24 +3,20 @@
<portal to="icon"><fa :icon="faStickyNote"/></portal>
<portal to="title">{{ $t('pages') }}</portal>
<mk-container :body-togglable="true">
<template #header><fa :icon="faEdit" fixed-width/>{{ $t('_pages.my') }}</template>
<div class="rknalgpo my">
<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
<mk-pagination :pagination="myPagesPagination" #default="{items}">
<mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
</mk-pagination>
</div>
</mk-container>
<mk-tab v-model="tab" :items="[{ label: $t('_pages.my'), value: 'my', icon: faEdit }, { label: $t('_pages.liked'), value: 'liked', icon: faHeart }]"/>
<mk-container :body-togglable="true">
<template #header><fa :icon="faHeart" fixed-width/>{{ $t('_pages.liked') }}</template>
<div class="rknalgpo">
<mk-pagination :pagination="likedPagesPagination" #default="{items}">
<mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
</mk-pagination>
</div>
</mk-container>
<div class="rknalgpo my" v-if="tab === 'my'">
<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
<mk-pagination :pagination="myPagesPagination" #default="{items}">
<mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
</mk-pagination>
</div>
<div class="rknalgpo" v-if="tab === 'liked'">
<mk-pagination :pagination="likedPagesPagination" #default="{items}">
<mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
</mk-pagination>
</div>
</div>
</template>
@@ -31,14 +27,15 @@ import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
import MkPagePreview from '../components/page-preview.vue';
import MkPagination from '../components/ui/pagination.vue';
import MkButton from '../components/ui/button.vue';
import MkContainer from '../components/ui/container.vue';
import MkTab from '../components/tab.vue';
export default defineComponent({
components: {
MkPagePreview, MkPagination, MkButton, MkContainer
MkPagePreview, MkPagination, MkButton, MkTab
},
data() {
return {
tab: 'my',
myPagesPagination: {
endpoint: 'i/pages',
limit: 5,

View File

@@ -18,6 +18,9 @@
<option v-for="x in $store.state.deviceUser.plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
</mk-select>
<template v-if="selectedPlugin">
<div style="margin: -8px 0 8px 0;">
<mk-switch :value="selectedPlugin.active" @change="changeActive(selectedPlugin, $event)">{{ $t('makeActive') }}</mk-switch>
</div>
<div class="_keyValue">
<div>{{ $t('version') }}:</div>
<div>{{ selectedPlugin.version }}</div>
@@ -44,11 +47,13 @@
import { defineComponent } from 'vue';
import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import MkSelect from '../../components/ui/select.vue';
import MkInfo from '../../components/ui/info.vue';
import MkSwitch from '../../components/ui/switch.vue';
export default defineComponent({
components: {
@@ -56,6 +61,7 @@ export default defineComponent({
MkTextarea,
MkSelect,
MkInfo,
MkSwitch,
},
data() {
@@ -101,8 +107,8 @@ export default defineComponent({
});
return;
}
const { id, name, version, author, description, permissions, config } = data;
if (id == null || name == null || version == null || author == null) {
const { name, version, author, description, permissions, config } = data;
if (name == null || version == null || author == null) {
this.$root.dialog({
type: 'error',
text: 'Required property not found :('
@@ -128,8 +134,9 @@ export default defineComponent({
});
this.$store.commit('deviceUser/installPlugin', {
id: uuid(),
meta: {
id, name, version, author, description, permissions, config
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
@@ -171,6 +178,17 @@ export default defineComponent({
config: result
});
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
this.$store.commit('deviceUser/changePluginActive', {
id: plugin.id,
active: active
});
this.$nextTick(() => {
location.reload();
});

View File

@@ -83,7 +83,7 @@
<router-view :user="user"></router-view>
<template v-if="$route.name == 'user'">
<div class="pins">
<x-note v-for="note in user.pinnedNotes" class="note" :note="note" :key="note.id" :detail="true" :pinned="true"/>
<x-note v-for="note in user.pinnedNotes" class="note" :note="note" @updated="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
</div>
<mk-container :body-togglable="true" class="content">
<template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
@@ -213,6 +213,11 @@ export default defineComponent({
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
},
pinnedNoteUpdated(oldValue, newValue) {
const i = this.user.pinnedNotes.findIndex(n => n === oldValue);
Vue.set(this.user.pinnedNotes, i, newValue);
},
number,
userPage

View File

@@ -15,9 +15,9 @@ export function createAiScriptEnv(vm, opts) {
text: text.value,
});
}),
'Mk:confirm': values.FN_NATIVE(async ([title, text]) => {
'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
const confirm = await vm.$root.dialog({
type: 'warning',
type: type ? type.value : 'question',
showCancelButton: true,
title: title.value,
text: text.value,
@@ -46,12 +46,13 @@ export function createAiScriptEnv(vm, opts) {
// TODO: vm引数は消せる(各種操作がstoreに移動し、かつstoreが複数ファイルで共有されるようになったため)
export function createPluginEnv(vm, opts) {
const config = new Map();
for (const [k, v] of Object.entries(opts.plugin.config)) {
for (const [k, v] of Object.entries(opts.plugin.config || {})) {
config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
}
return {
...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }),
//#region Deprecated
'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
}),
@@ -61,6 +62,25 @@ export function createPluginEnv(vm, opts) {
'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
}),
//#endregion
'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
}),
'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler });
}),
'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
}),
'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
vm.$store.commit('registerNoteViewInterruptor', { pluginId: opts.plugin.id, handler });
}),
'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
vm.$store.commit('registerNotePostInterruptor', { pluginId: opts.plugin.id, handler });
}),
'Plugin:open_url': values.FN_NATIVE(([url]) => {
window.open(url.value, '_blank');
}),
'Plugin:config': values.OBJ(config),
};
}

View File

@@ -0,0 +1,26 @@
export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> {
// 自分自身
if (me && (note.userId === me.id)) return false;
const words = mutedWords
// Clean up
.map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0);
if (words.length > 0) {
if (note.text == null) return false;
const matched = words.some(and =>
and.every(keyword => {
const regexp = keyword.match(/^\/(.+)\/(.*)$/);
if (regexp) {
return new RegExp(regexp[1], regexp[2]).test(note.text!);
}
return note.text!.includes(keyword);
}));
if (matched) return true;
}
return false;
}

View File

@@ -73,10 +73,6 @@ export default (opts) => ({
},
methods: {
updateItem(i, item) {
(this as any).items[i] = item;
},
reload() {
this.items = [];
this.init();
@@ -93,6 +89,9 @@ export default (opts) => ({
...params,
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
}).then(items => {
for (const item of items) {
Object.freeze(item);
}
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
items.pop();
this.items = this.pagination.reversed ? [...items].reverse() : items;
@@ -129,6 +128,9 @@ export default (opts) => ({
untilId: this.items[this.items.length - 1].id,
}),
}).then(items => {
for (const item of items) {
Object.freeze(item);
}
if (items.length > SECOND_FETCH_LIMIT) {
items.pop();
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);

View File

@@ -109,10 +109,10 @@ export default class Stream extends EventEmitter {
}
for (const c of connections.filter(c => c != null)) {
c.emit(body.type, body.body);
c.emit(body.type, Object.freeze(body.body));
}
} else {
this.emit(type, body);
this.emit(type, Object.freeze(body));
}
}

View File

@@ -18,6 +18,7 @@ export const defaultSettings = {
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
memo: null,
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
mutedWords: [],
};
export const defaultDeviceUserSettings = {
@@ -44,7 +45,14 @@ export const defaultDeviceUserSettings = {
columns: [],
layout: [],
},
plugins: [],
plugins: [] as {
id: string;
name: string;
active: boolean;
configData: Record<string, any>;
token: string;
ast: any[];
}[],
};
export const defaultDeviceSettings = {
@@ -110,6 +118,8 @@ export const store = createStore({
postFormActions: [],
userActions: [],
noteActions: [],
noteViewInterruptors: [],
notePostInterruptors: [],
},
getters: {
@@ -277,6 +287,22 @@ export const store = createStore({
}
});
},
registerNoteViewInterruptor(state, { pluginId, handler }) {
state.noteViewInterruptors.push({
handler: (note) => {
return state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
}
});
},
registerNotePostInterruptor(state, { pluginId, handler }) {
state.notePostInterruptors.push({
handler: (note) => {
return state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
}
});
},
},
actions: {
@@ -598,9 +624,11 @@ export const store = createStore({
},
//#endregion
installPlugin(state, { meta, ast, token }) {
installPlugin(state, { id, meta, ast, token }) {
state.plugins.push({
...meta,
id,
active: true,
configData: {},
token: token,
ast: ast
@@ -614,6 +642,10 @@ export const store = createStore({
configPlugin(state, { id, config }) {
state.plugins.find(p => p.id === id).configData = config;
},
changePluginActive(state, { id, active }) {
state.plugins.find(p => p.id === id).active = active;
},
}
},

View File

@@ -355,6 +355,10 @@ hr {
padding: 16px;
}
&._noPad {
padding: 0 !important;
}
& + ._content {
border-top: solid 1px var(--divider);
}

View File

@@ -5,12 +5,13 @@
<div class="wbrkwalb">
<mk-loading v-if="fetching"/>
<transition-group tag="div" name="chart" class="instances" v-else>
<div v-for="instance in instances" :key="instance.id">
<div class="instance">
<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">#{{ instance.host }}</a>
<p>{{ instance.softwareName }} {{ instance.softwareVersion }}</p>
<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
<div class="body">
<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
</div>
<x-chart class="chart" :src="stat.chart"/>
<mk-mini-chart class="chart" :src="charts[i].requests.received"/>
</div>
</transition-group>
</div>
@@ -21,7 +22,7 @@
import { faGlobe } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue';
import define from './define';
import XChart from './trends.chart.vue';
import MkMiniChart from '../components/mini-chart.vue';
export default define({
name: 'federation',
@@ -33,11 +34,12 @@ export default define({
})
}).extend({
components: {
MkContainer, XChart
MkContainer, MkMiniChart
},
data() {
return {
instances: [],
charts: [],
fetching: true,
faGlobe
};
@@ -50,14 +52,15 @@ export default define({
clearInterval(this.clock);
},
methods: {
fetch() {
this.$root.api('federation/instances', {
async fetch() {
const instances = await this.$root.api('federation/instances', {
sort: '+lastCommunicatedAt',
limit: 5
}).then(instances => {
this.instances = instances;
this.fetching = false;
});
const charts = await Promise.all(instances.map(i => this.$root.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
this.instances = instances;
this.charts = charts;
this.fetching = false;
}
}
});
@@ -65,6 +68,9 @@ export default define({
<style lang="scss" scoped>
.wbrkwalb {
$bodyTitleHieght: 18px;
$bodyInfoHieght: 16px;
height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
overflow: hidden;
@@ -73,13 +79,22 @@ export default define({
transition: transform 1s ease;
}
> div {
> .instance {
display: flex;
align-items: center;
padding: 14px 16px;
border-bottom: solid 1px var(--divider);
> .instance {
> img {
display: block;
width: ($bodyTitleHieght + $bodyInfoHieght);
height: ($bodyTitleHieght + $bodyInfoHieght);
object-fit: cover;
border-radius: 4px;
margin-right: 8px;
}
> .body {
flex: 1;
overflow: hidden;
font-size: 0.9em;
@@ -91,14 +106,14 @@ export default define({
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 18px;
line-height: $bodyTitleHieght;
}
> p {
margin: 0;
font-size: 75%;
opacity: 0.7;
line-height: 16px;
line-height: $bodyInfoHieght;
}
}

View File

@@ -10,7 +10,7 @@
<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
</div>
<x-chart class="chart" :src="stat.chart"/>
<mk-mini-chart class="chart" :src="stat.chart"/>
</div>
</transition-group>
</div>
@@ -21,7 +21,7 @@
import { faHashtag } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue';
import define from './define';
import XChart from './trends.chart.vue';
import MkMiniChart from '../components/mini-chart.vue';
export default define({
name: 'hashtags',
@@ -33,7 +33,7 @@ export default define({
})
}).extend({
components: {
MkContainer, XChart
MkContainer, MkMiniChart
},
data() {
return {