Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
195f676500 | ||
![]() |
a9a2f4820b | ||
![]() |
8414db57f0 | ||
![]() |
609d68933e | ||
![]() |
a23b8cebbc | ||
![]() |
89f6b03cd6 | ||
![]() |
7bc9de03a6 | ||
![]() |
3c865d6054 | ||
![]() |
fd770b008e | ||
![]() |
b0d60ef2c2 | ||
![]() |
7b9cea06ef | ||
![]() |
30608d3e22 | ||
![]() |
8bf4e55338 | ||
![]() |
6ead1de383 | ||
![]() |
3b628ec3c4 | ||
![]() |
0ed704d173 | ||
![]() |
87b6ef0ec5 | ||
![]() |
5184a07cf2 | ||
![]() |
dba04cc59c | ||
![]() |
f4045fb5b3 | ||
![]() |
16c36163b4 | ||
![]() |
1ac033ff18 | ||
![]() |
ccfd48232a | ||
![]() |
429bf179dc | ||
![]() |
8ba3fb13eb | ||
![]() |
11496d887e |
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "8.50.0",
|
"version": "8.55.0",
|
||||||
"clientVersion": "1.0.9883",
|
"clientVersion": "1.0.9909",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -217,9 +217,9 @@
|
|||||||
"vuewordcloud": "18.7.11",
|
"vuewordcloud": "18.7.11",
|
||||||
"vuex": "3.0.1",
|
"vuex": "3.0.1",
|
||||||
"vuex-persistedstate": "2.5.4",
|
"vuex-persistedstate": "2.5.4",
|
||||||
"web-push": "3.3.2",
|
"web-push": "3.3.3",
|
||||||
"webfinger.js": "2.6.6",
|
"webfinger.js": "2.6.6",
|
||||||
"webpack": "4.19.0",
|
"webpack": "4.19.1",
|
||||||
"webpack-cli": "3.1.0",
|
"webpack-cli": "3.1.0",
|
||||||
"websocket": "1.0.26",
|
"websocket": "1.0.26",
|
||||||
"ws": "6.0.0",
|
"ws": "6.0.0",
|
||||||
|
@@ -1,3 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view id="app"></router-view>
|
<router-view id="app" v-hotkey.global="keymap"></router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { url, lang } from './config';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
computed: {
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'h|slash': this.help
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
help() {
|
||||||
|
window.open(`${url}/docs/${lang}/keyboard-shortcut`, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@@ -22,7 +22,10 @@ const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): a
|
|||||||
|
|
||||||
result.patterns = patterns.split('|').map(part => {
|
result.patterns = patterns.split('|').map(part => {
|
||||||
const pattern = {
|
const pattern = {
|
||||||
which: []
|
which: [],
|
||||||
|
ctrl: false,
|
||||||
|
alt: false,
|
||||||
|
shift: false
|
||||||
} as pattern;
|
} as pattern;
|
||||||
|
|
||||||
part.trim().split('+').forEach(key => {
|
part.trim().split('+').forEach(key => {
|
||||||
@@ -66,10 +69,10 @@ export default {
|
|||||||
if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
|
if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
|
||||||
|
|
||||||
const matched = action.patterns.some(pattern => {
|
const matched = action.patterns.some(pattern => {
|
||||||
let matched = pattern.which.includes(key);
|
const matched = pattern.which.includes(key) &&
|
||||||
if (pattern.ctrl && !e.ctrlKey) matched = false;
|
pattern.ctrl == e.ctrlKey &&
|
||||||
if (pattern.shift && !e.shiftKey) matched = false;
|
pattern.shift == e.shiftKey &&
|
||||||
if (pattern.alt && !e.altKey) matched = false;
|
pattern.alt == e.altKey;
|
||||||
|
|
||||||
if (matched) {
|
if (matched) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
|
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
|
||||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||||
<div class="popover" :class="{ hukidasi }" ref="popover">
|
<div class="popover" :class="{ hukidasi }" ref="popover">
|
||||||
<template v-for="item in items">
|
<template v-for="item, i in items">
|
||||||
<div v-if="item === null"></div>
|
<div v-if="item === null"></div>
|
||||||
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text"></button>
|
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text" :tabindex="i"></button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -77,10 +77,10 @@ export default Vue.extend({
|
|||||||
return {
|
return {
|
||||||
'esc': this.close,
|
'esc': this.close,
|
||||||
'enter|space|plus': this.choose,
|
'enter|space|plus': this.choose,
|
||||||
'up': this.focusUp,
|
'up|k': this.focusUp,
|
||||||
'right': this.focusRight,
|
'left|h|shift+tab': this.focusLeft,
|
||||||
'down': this.focusDown,
|
'right|l|tab': this.focusRight,
|
||||||
'left': this.focusLeft,
|
'down|j': this.focusDown,
|
||||||
'1': () => this.react('like'),
|
'1': () => this.react('like'),
|
||||||
'2': () => this.react('love'),
|
'2': () => this.react('love'),
|
||||||
'3': () => this.react('laugh'),
|
'3': () => this.react('laugh'),
|
||||||
@@ -97,10 +97,10 @@ export default Vue.extend({
|
|||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
focus(i) {
|
focus(i) {
|
||||||
this.$refs.buttons.childNodes[i].focus();
|
this.$refs.buttons.children[i].focus();
|
||||||
|
|
||||||
if (this.showFocus) {
|
if (this.showFocus) {
|
||||||
this.title = this.$refs.buttons.childNodes[i].title;
|
this.title = this.$refs.buttons.children[i].title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -40,18 +40,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
||||||
<button class="replyButton" @click="reply" title="%i18n:@reply%">
|
<button class="replyButton" @click="reply()" title="%i18n:@reply%">
|
||||||
<template v-if="p.reply">%fa:reply-all%</template>
|
<template v-if="p.reply">%fa:reply-all%</template>
|
||||||
<template v-else>%fa:reply%</template>
|
<template v-else>%fa:reply%</template>
|
||||||
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="renoteButton" @click="renote" title="%i18n:@renote%">
|
<button class="renoteButton" @click="renote()" title="%i18n:@renote%">
|
||||||
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
|
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
|
||||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button @click="menu" ref="menuButton">
|
<button @click="menu()" ref="menuButton">
|
||||||
%fa:ellipsis-h%
|
%fa:ellipsis-h%
|
||||||
</button>
|
</button>
|
||||||
<!-- <button title="%i18n:@detail">
|
<!-- <button title="%i18n:@detail">
|
||||||
@@ -113,11 +113,25 @@ export default Vue.extend({
|
|||||||
computed: {
|
computed: {
|
||||||
keymap(): any {
|
keymap(): any {
|
||||||
return {
|
return {
|
||||||
'r|left': this.reply,
|
'r|left': () => this.reply(true),
|
||||||
'a|plus': () => this.react(true),
|
'e|a|plus': () => this.react(true),
|
||||||
'n|right': this.renote,
|
'q|right': () => this.renote(true),
|
||||||
'up|shift+tab': this.focusBefore,
|
'ctrl+q|ctrl+right': this.renoteDirectly,
|
||||||
'down|tab': this.focusAfter,
|
'up|k|shift+tab': this.focusBefore,
|
||||||
|
'down|j|tab': this.focusAfter,
|
||||||
|
'esc': this.blur,
|
||||||
|
'm|o': () => this.menu(true),
|
||||||
|
's': this.toggleShowContent,
|
||||||
|
'1': () => this.reactDirectly('like'),
|
||||||
|
'2': () => this.reactDirectly('love'),
|
||||||
|
'3': () => this.reactDirectly('laugh'),
|
||||||
|
'4': () => this.reactDirectly('hmm'),
|
||||||
|
'5': () => this.reactDirectly('surprise'),
|
||||||
|
'6': () => this.reactDirectly('congrats'),
|
||||||
|
'7': () => this.reactDirectly('angry'),
|
||||||
|
'8': () => this.reactDirectly('confused'),
|
||||||
|
'9': () => this.reactDirectly('rip'),
|
||||||
|
'0': () => this.reactDirectly('pudding'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -230,18 +244,26 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reply() {
|
reply(viaKeyboard = false) {
|
||||||
(this as any).os.new(MkPostFormWindow, {
|
(this as any).os.new(MkPostFormWindow, {
|
||||||
reply: this.p
|
reply: this.p,
|
||||||
|
animation: !viaKeyboard
|
||||||
}).$once('closed', this.focus);
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
renote() {
|
renote(viaKeyboard = false) {
|
||||||
(this as any).os.new(MkRenoteFormWindow, {
|
(this as any).os.new(MkRenoteFormWindow, {
|
||||||
note: this.p
|
note: this.p,
|
||||||
|
animation: !viaKeyboard
|
||||||
}).$once('closed', this.focus);
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renoteDirectly() {
|
||||||
|
(this as any).api('notes/create', {
|
||||||
|
renoteId: this.p.id
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
react(viaKeyboard = false) {
|
react(viaKeyboard = false) {
|
||||||
this.blur();
|
this.blur();
|
||||||
(this as any).os.new(MkReactionPicker, {
|
(this as any).os.new(MkReactionPicker, {
|
||||||
@@ -252,13 +274,25 @@ export default Vue.extend({
|
|||||||
}).$once('closed', this.focus);
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
menu() {
|
reactDirectly(reaction) {
|
||||||
|
(this as any).api('notes/reactions/create', {
|
||||||
|
noteId: this.p.id,
|
||||||
|
reaction: reaction
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
menu(viaKeyboard = false) {
|
||||||
(this as any).os.new(MkNoteMenu, {
|
(this as any).os.new(MkNoteMenu, {
|
||||||
source: this.$refs.menuButton,
|
source: this.$refs.menuButton,
|
||||||
note: this.p
|
note: this.p,
|
||||||
|
animation: !viaKeyboard
|
||||||
}).$once('closed', this.focus);
|
}).$once('closed', this.focus);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleShowContent() {
|
||||||
|
this.showContent = !this.showContent;
|
||||||
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.$el.focus();
|
this.$el.focus();
|
||||||
},
|
},
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div">
|
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes">
|
||||||
<template v-for="(note, i) in _notes">
|
<template v-for="(note, i) in _notes">
|
||||||
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
|
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
|
||||||
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||||
@@ -89,7 +89,7 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
(this.$refs.note as any)[0].focus();
|
(this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
onNoteUpdated(i, note) {
|
onNoteUpdated(i, note) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed">
|
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation">
|
||||||
<span slot="header" class="mk-post-form-window--header">
|
<span slot="header" class="mk-post-form-window--header">
|
||||||
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
|
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
|
||||||
<span v-if="!reply">%i18n:@note%</span>
|
<span v-if="!reply">%i18n:@note%</span>
|
||||||
@@ -25,7 +25,19 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['reply'],
|
props: {
|
||||||
|
reply: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
|
||||||
|
animation: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
uploadings: [],
|
uploadings: [],
|
||||||
@@ -33,11 +45,13 @@ export default Vue.extend({
|
|||||||
geo: null
|
geo: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
(this.$refs.form as any).focus();
|
(this.$refs.form as any).focus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onChangeUploadings(files) {
|
onChangeUploadings(files) {
|
||||||
this.uploadings = files;
|
this.uploadings = files;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal @closed="onWindowClosed">
|
<mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation">
|
||||||
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
|
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
|
||||||
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
|
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
|
||||||
</mk-window>
|
</mk-window>
|
||||||
@@ -9,13 +9,25 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['note'],
|
props: {
|
||||||
|
note: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
animation: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
keymap(): any {
|
keymap(): any {
|
||||||
return {
|
return {
|
||||||
'esc': this.close,
|
'esc': this.close,
|
||||||
'ctrl+enter': this.post
|
'enter': this.post,
|
||||||
|
'q': this.quote,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -24,6 +36,9 @@ export default Vue.extend({
|
|||||||
post() {
|
post() {
|
||||||
(this.$refs.form as any).ok();
|
(this.$refs.form as any).ok();
|
||||||
},
|
},
|
||||||
|
quote() {
|
||||||
|
(this.$refs.form as any).onQuote();
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
(this.$refs.window as any).close();
|
(this.$refs.window as any).close();
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="account">
|
<div class="account" v-hotkey.global="keymap">
|
||||||
<button class="header" :data-active="isOpen" @click="toggle">
|
<button class="header" :data-active="isOpen" @click="toggle">
|
||||||
<span class="username">{{ $store.state.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
|
<span class="username">{{ $store.state.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
|
||||||
<mk-avatar class="avatar" :user="$store.state.i"/>
|
<mk-avatar class="avatar" :user="$store.state.i"/>
|
||||||
@@ -63,6 +63,13 @@ export default Vue.extend({
|
|||||||
isOpen: false
|
isOpen: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'a|m': this.toggle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="notifications">
|
<div class="notifications" v-hotkey.global="keymap">
|
||||||
<button :data-active="isOpen" @click="toggle" title="%i18n:@title%">
|
<button :data-active="isOpen" @click="toggle" title="%i18n:@title%">
|
||||||
%fa:R bell%<template v-if="hasUnreadNotification">%fa:circle%</template>
|
%fa:R bell%<template v-if="hasUnreadNotification">%fa:circle%</template>
|
||||||
</button>
|
</button>
|
||||||
@@ -19,11 +19,19 @@ export default Vue.extend({
|
|||||||
isOpen: false
|
isOpen: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hasUnreadNotification(): boolean {
|
hasUnreadNotification(): boolean {
|
||||||
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
|
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
|
||||||
|
},
|
||||||
|
|
||||||
|
keymap(): any {
|
||||||
|
return {
|
||||||
|
'shift+n': this.toggle
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggle() {
|
toggle() {
|
||||||
this.isOpen ? this.close() : this.open();
|
this.isOpen ? this.close() : this.open();
|
||||||
|
@@ -76,6 +76,11 @@ export default Vue.extend({
|
|||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -142,7 +147,7 @@ export default Vue.extend({
|
|||||||
anime({
|
anime({
|
||||||
targets: bg,
|
targets: bg,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
duration: 100,
|
duration: this.animation ? 100 : 0,
|
||||||
easing: 'linear'
|
easing: 'linear'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,7 +157,7 @@ export default Vue.extend({
|
|||||||
targets: main,
|
targets: main,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: [1.1, 1],
|
scale: [1.1, 1],
|
||||||
duration: 200,
|
duration: this.animation ? 200 : 0,
|
||||||
easing: 'easeOutQuad'
|
easing: 'easeOutQuad'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,7 +165,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$emit('opened');
|
this.$emit('opened');
|
||||||
}, 300);
|
}, this.animation ? 300 : 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@@ -174,7 +179,7 @@ export default Vue.extend({
|
|||||||
anime({
|
anime({
|
||||||
targets: bg,
|
targets: bg,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 300,
|
duration: this.animation ? 300 : 0,
|
||||||
easing: 'linear'
|
easing: 'linear'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -185,14 +190,14 @@ export default Vue.extend({
|
|||||||
targets: main,
|
targets: main,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
scale: 0.8,
|
scale: 0.8,
|
||||||
duration: 300,
|
duration: this.animation ? 300 : 0,
|
||||||
easing: [0.5, -0.5, 1, 0.5]
|
easing: [0.5, -0.5, 1, 0.5]
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$emit('closed');
|
this.$emit('closed');
|
||||||
this.destroyDom();
|
this.destroyDom();
|
||||||
}, 300);
|
}, this.animation ? 300 : 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
popout() {
|
popout() {
|
||||||
|
@@ -35,7 +35,7 @@
|
|||||||
<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
|
<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
|
||||||
<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span>
|
<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span>
|
||||||
<template v-if="lists">
|
<template v-if="lists">
|
||||||
<div class="hr"></div>
|
<div class="hr" v-if="lists.length > 0"></div>
|
||||||
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
|
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
|
<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
|
||||||
|
96
src/docs/keyboard-shortcut.ja-JP.md
Normal file
96
src/docs/keyboard-shortcut.ja-JP.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Misskeyキーボードショートカットまとめ
|
||||||
|
|
||||||
|
## グローバル
|
||||||
|
これらのショートカットは基本的にどこでも使えます。
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
|
||||||
|
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
|
||||||
|
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
|
||||||
|
<tr><td><kbd class="key">A</kbd>, <kbd class="key">M</kbd></td><td>アカウントメニューを表示/隠す</td><td><b>A</b>ccount, <b>M</b>y, <b>M</b>e, <b>M</b>enu</td></tr>
|
||||||
|
<tr><td><kbd class="key">Z</kbd></td><td>上部のバーを隠す</td><td><b>Z</b>en</td></tr>
|
||||||
|
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 投稿にフォーカスされた状態
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>上の投稿にフォーカスを移動</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">↓</kbd>, <kbd class="key">J</kbd>, <kbd class="key">Tab</kbd></td><td>下の投稿にフォーカスを移動</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">←</kbd>, <kbd class="key">R</kbd></td><td>返信フォームを開く</td><td><b>R</b>eply</td></tr>
|
||||||
|
<tr><td><kbd class="key">→</kbd>, <kbd class="key">Q</kbd></td><td>Renoteフォームを開く</td><td><b>Q</b>uote</td></tr>
|
||||||
|
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">→</kbd></kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</td></tr>
|
||||||
|
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>投稿に対するメニューを開く</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
|
||||||
|
<tr><td><kbd class="key">S</kbd></td><td>CWで隠された部分を表示 or 隠す</td><td><b>S</b>how, <b>S</b>ee</td></tr>
|
||||||
|
<tr><td><kbd class="key">Esc</kbd></td><td>フォーカスを外す</td><td>-</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Renoteフォーム
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><kbd class="key">Enter</kbd></td><td>Renoteする</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">Q</kbd></td><td>フォームを展開する</td><td><b>Q</b>uote</td></tr>
|
||||||
|
<tr><td><kbd class="key">Esc</kbd></td><td>フォームを閉じる</td><td>-</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## リアクションフォーム
|
||||||
|
デフォルトで「👍」にフォーカスが当たっている状態です。
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd></td><td>上のリアクションにフォーカスを移動</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">↓</kbd>, <kbd class="key">J</kbd></td><td>下のリアクションにフォーカスを移動</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">←</kbd>, <kbd class="key">H</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>左のリアクションにフォーカスを移動</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">→</kbd>, <kbd class="key">L</kbd>, <kbd class="key">Tab</kbd></td><td>右のリアクションにフォーカスを移動</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">Enter</kbd>, <kbd class="key">Space</kbd>, <kbd class="key">+</kbd></td><td>リアクション確定</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションで確定(対応については後述)</td><td>-</td></tr>
|
||||||
|
<tr><td><kbd class="key">Esc</kbd></td><td>リアクションするのをやめる</td><td>-</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## リアクションと数字キーの対応
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>数字キー</th><th>リアクション</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><kbd class="key">1</kbd></td><td>👍</td></tr>
|
||||||
|
<tr><td><kbd class="key">2</kbd></td><td>❤️</td></tr>
|
||||||
|
<tr><td><kbd class="key">3</kbd></td><td>😆</td></tr>
|
||||||
|
<tr><td><kbd class="key">4</kbd></td><td>🤔</td></tr>
|
||||||
|
<tr><td><kbd class="key">5</kbd></td><td>😮</td></tr>
|
||||||
|
<tr><td><kbd class="key">6</kbd></td><td>🎉</td></tr>
|
||||||
|
<tr><td><kbd class="key">7</kbd></td><td>💢</td></tr>
|
||||||
|
<tr><td><kbd class="key">8</kbd></td><td>😥</td></tr>
|
||||||
|
<tr><td><kbd class="key">9</kbd></td><td>😇</td></tr>
|
||||||
|
<tr><td><kbd class="key">0</kbd></td><td>🍮 or 🍣</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
# 例
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>ショートカット</th><th>動作</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><kbd class="key">t</kbd><kbd class="key">+</kbd><kbd class="key">+</kbd></td><td>タイムラインの最新の投稿に👍する</td></tr>
|
||||||
|
<tr><td><kbd class="key">t</kbd><kbd class="key">1</kbd></td><td>タイムラインの最新の投稿に👍する</td></tr>
|
||||||
|
<tr><td><kbd class="key">t</kbd><kbd class="key">0</kbd></td><td>タイムラインの最新の投稿に🍮する</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
@@ -128,3 +128,24 @@ pre
|
|||||||
> code
|
> code
|
||||||
display block
|
display block
|
||||||
padding 16px
|
padding 16px
|
||||||
|
|
||||||
|
kbd.group
|
||||||
|
display inline-block
|
||||||
|
padding 4px
|
||||||
|
background #fbfbfb
|
||||||
|
border 1px solid #d6d6d6
|
||||||
|
border-radius 4px
|
||||||
|
box-shadow 0 1px 1px rgba(0, 0, 0, 0.1)
|
||||||
|
|
||||||
|
kbd.key
|
||||||
|
display inline-block
|
||||||
|
padding 6px 8px
|
||||||
|
background #fff
|
||||||
|
border solid 1px #cecece
|
||||||
|
border-radius 4px
|
||||||
|
box-shadow 0 1px 1px rgba(0, 0, 0, 0.1)
|
||||||
|
|
||||||
|
td
|
||||||
|
> kbd.group,
|
||||||
|
> kbd.key
|
||||||
|
margin 4px
|
||||||
|
9
src/remote/activitypub/renderer/add.ts
Normal file
9
src/remote/activitypub/renderer/add.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import config from '../../../config';
|
||||||
|
import { ILocalUser } from '../../../models/user';
|
||||||
|
|
||||||
|
export default (user: ILocalUser, target: any, object: any) => ({
|
||||||
|
type: 'Add',
|
||||||
|
actor: `${config.url}/users/${user._id}`,
|
||||||
|
target,
|
||||||
|
object
|
||||||
|
});
|
@@ -4,8 +4,9 @@
|
|||||||
* @param totalItems Total number of items
|
* @param totalItems Total number of items
|
||||||
* @param first URL of first page (optional)
|
* @param first URL of first page (optional)
|
||||||
* @param last URL of last page (optional)
|
* @param last URL of last page (optional)
|
||||||
|
* @param orderedItems attached objects (optional)
|
||||||
*/
|
*/
|
||||||
export default function(id: string, totalItems: any, first: string, last: string) {
|
export default function(id: string, totalItems: any, first?: string, last?: string, orderedItems?: object) {
|
||||||
const page: any = {
|
const page: any = {
|
||||||
id,
|
id,
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
@@ -14,6 +15,7 @@ export default function(id: string, totalItems: any, first: string, last: string
|
|||||||
|
|
||||||
if (first) page.first = first;
|
if (first) page.first = first;
|
||||||
if (last) page.last = last;
|
if (last) page.last = last;
|
||||||
|
if (orderedItems) page.orderedItems = orderedItems;
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ export default async (user: ILocalUser) => {
|
|||||||
outbox: `${id}/outbox`,
|
outbox: `${id}/outbox`,
|
||||||
followers: `${id}/followers`,
|
followers: `${id}/followers`,
|
||||||
following: `${id}/following`,
|
following: `${id}/following`,
|
||||||
|
featured: `${id}/collections/featured`,
|
||||||
sharedInbox: `${config.url}/inbox`,
|
sharedInbox: `${config.url}/inbox`,
|
||||||
url: `${config.url}/@${user.username}`,
|
url: `${config.url}/@${user.username}`,
|
||||||
preferredUsername: user.username,
|
preferredUsername: user.username,
|
||||||
|
9
src/remote/activitypub/renderer/remove.ts
Normal file
9
src/remote/activitypub/renderer/remove.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import config from '../../../config';
|
||||||
|
import { ILocalUser } from '../../../models/user';
|
||||||
|
|
||||||
|
export default (user: ILocalUser, target: any, object: any) => ({
|
||||||
|
type: 'Remove',
|
||||||
|
actor: `${config.url}/users/${user._id}`,
|
||||||
|
target,
|
||||||
|
object
|
||||||
|
});
|
@@ -53,6 +53,7 @@ export interface IPerson extends IObject {
|
|||||||
publicKey: any;
|
publicKey: any;
|
||||||
followers: any;
|
followers: any;
|
||||||
following: any;
|
following: any;
|
||||||
|
featured?: any;
|
||||||
outbox: any;
|
outbox: any;
|
||||||
endpoints: string[];
|
endpoints: string[];
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import renderPerson from '../remote/activitypub/renderer/person';
|
|||||||
import Outbox, { packActivity } from './activitypub/outbox';
|
import Outbox, { packActivity } from './activitypub/outbox';
|
||||||
import Followers from './activitypub/followers';
|
import Followers from './activitypub/followers';
|
||||||
import Following from './activitypub/following';
|
import Following from './activitypub/following';
|
||||||
|
import Featured from './activitypub/featured';
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
@@ -74,6 +75,7 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = pack(await renderNote(note, false));
|
ctx.body = pack(await renderNote(note, false));
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ router.get('/notes/:note/activity', async ctx => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = pack(await packActivity(note));
|
ctx.body = pack(await packActivity(note));
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,6 +105,9 @@ router.get('/users/:user/followers', Followers);
|
|||||||
// following
|
// following
|
||||||
router.get('/users/:user/following', Following);
|
router.get('/users/:user/following', Following);
|
||||||
|
|
||||||
|
// featured
|
||||||
|
router.get('/users/:user/collections/featured', Featured);
|
||||||
|
|
||||||
// publickey
|
// publickey
|
||||||
router.get('/users/:user/publickey', async ctx => {
|
router.get('/users/:user/publickey', async ctx => {
|
||||||
const userId = new mongo.ObjectID(ctx.params.user);
|
const userId = new mongo.ObjectID(ctx.params.user);
|
||||||
@@ -118,6 +124,7 @@ router.get('/users/:user/publickey', async ctx => {
|
|||||||
|
|
||||||
if (isLocalUser(user)) {
|
if (isLocalUser(user)) {
|
||||||
ctx.body = pack(renderKey(user));
|
ctx.body = pack(renderKey(user));
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
@@ -132,6 +139,7 @@ async function userInfo(ctx: Router.IRouterContext, user: IUser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = pack(await renderPerson(user as ILocalUser));
|
ctx.body = pack(await renderPerson(user as ILocalUser));
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
39
src/server/activitypub/featured.ts
Normal file
39
src/server/activitypub/featured.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import * as Router from 'koa-router';
|
||||||
|
import config from '../../config';
|
||||||
|
import User from '../../models/user';
|
||||||
|
import pack from '../../remote/activitypub/renderer';
|
||||||
|
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
|
||||||
|
import { setResponseType } from '../activitypub';
|
||||||
|
import Note from '../../models/note';
|
||||||
|
import renderNote from '../../remote/activitypub/renderer/note';
|
||||||
|
|
||||||
|
export default async (ctx: Router.IRouterContext) => {
|
||||||
|
const userId = new mongo.ObjectID(ctx.params.user);
|
||||||
|
|
||||||
|
// Verify user
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: userId,
|
||||||
|
host: null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinnedNoteIds = user.pinnedNoteIds || [];
|
||||||
|
|
||||||
|
const pinnedNotes = await Promise.all(pinnedNoteIds.map(id => Note.findOne({ _id: id })));
|
||||||
|
|
||||||
|
const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
|
||||||
|
|
||||||
|
const rendered = renderOrderedCollection(
|
||||||
|
`${config.url}/users/${userId}/collections/featured`,
|
||||||
|
renderedNotes.length, null, null, renderedNotes
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.body = pack(rendered);
|
||||||
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
|
setResponseType(ctx);
|
||||||
|
};
|
@@ -78,6 +78,7 @@ export default async (ctx: Router.IRouterContext) => {
|
|||||||
// index page
|
// index page
|
||||||
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null);
|
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null);
|
||||||
ctx.body = pack(rendered);
|
ctx.body = pack(rendered);
|
||||||
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -78,6 +78,7 @@ export default async (ctx: Router.IRouterContext) => {
|
|||||||
// index page
|
// index page
|
||||||
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null);
|
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null);
|
||||||
ctx.body = pack(rendered);
|
ctx.body = pack(rendered);
|
||||||
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -88,6 +88,7 @@ export default async (ctx: Router.IRouterContext) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ctx.body = pack(rendered);
|
ctx.body = pack(rendered);
|
||||||
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
} else {
|
} else {
|
||||||
// index page
|
// index page
|
||||||
@@ -96,6 +97,7 @@ export default async (ctx: Router.IRouterContext) => {
|
|||||||
`${partOf}?page=true&since_id=000000000000000000000000`
|
`${partOf}?page=true&since_id=000000000000000000000000`
|
||||||
);
|
);
|
||||||
ctx.body = pack(rendered);
|
ctx.body = pack(rendered);
|
||||||
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
|
import * as mongo from 'mongodb';
|
||||||
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
|
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
|
||||||
import User, { ILocalUser } from '../../../../models/user';
|
import User, { ILocalUser } from '../../../../models/user';
|
||||||
import Note from '../../../../models/note';
|
import Note from '../../../../models/note';
|
||||||
import { pack } from '../../../../models/user';
|
import { pack } from '../../../../models/user';
|
||||||
|
import { deliverPinnedChange } from '../../../../services/i/pin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pin note
|
* Pin note
|
||||||
@@ -21,6 +23,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
|
|||||||
return rej('note not found');
|
return rej('note not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let addedId: mongo.ObjectID;
|
||||||
|
let removedId: mongo.ObjectID;
|
||||||
|
|
||||||
const pinnedNoteIds = user.pinnedNoteIds || [];
|
const pinnedNoteIds = user.pinnedNoteIds || [];
|
||||||
|
|
||||||
if (pinnedNoteIds.some(id => id.equals(note._id))) {
|
if (pinnedNoteIds.some(id => id.equals(note._id))) {
|
||||||
@@ -28,9 +33,10 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pinnedNoteIds.unshift(note._id);
|
pinnedNoteIds.unshift(note._id);
|
||||||
|
addedId = note._id;
|
||||||
|
|
||||||
if (pinnedNoteIds.length > 5) {
|
if (pinnedNoteIds.length > 5) {
|
||||||
pinnedNoteIds.pop();
|
removedId = pinnedNoteIds.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
await User.update(user._id, {
|
await User.update(user._id, {
|
||||||
@@ -44,6 +50,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
|
|||||||
detail: true
|
detail: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Send Add/Remove to followers
|
||||||
|
deliverPinnedChange(user._id, removedId, addedId);
|
||||||
|
|
||||||
// Send response
|
// Send response
|
||||||
res(iObj);
|
res(iObj);
|
||||||
});
|
});
|
||||||
|
61
src/services/i/pin.ts
Normal file
61
src/services/i/pin.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import config from '../../config';
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import User, { isLocalUser, isRemoteUser, ILocalUser } from '../../models/user';
|
||||||
|
import Following from '../../models/following';
|
||||||
|
import renderAdd from '../../remote/activitypub/renderer/add';
|
||||||
|
import renderRemove from '../../remote/activitypub/renderer/remove';
|
||||||
|
import packAp from '../../remote/activitypub/renderer';
|
||||||
|
import { deliver } from '../../queue';
|
||||||
|
|
||||||
|
export async function deliverPinnedChange(userId: mongo.ObjectID, oldId?: mongo.ObjectID, newId?: mongo.ObjectID) {
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isLocalUser(user)) return;
|
||||||
|
|
||||||
|
const queue = await CreateRemoteInboxes(user);
|
||||||
|
|
||||||
|
if (queue.length < 1) return;
|
||||||
|
|
||||||
|
const target = `${config.url}/users/${user._id}/collections/featured`;
|
||||||
|
|
||||||
|
if (oldId) {
|
||||||
|
const oldItem = `${config.url}/notes/${oldId}`;
|
||||||
|
const content = packAp(renderRemove(user, target, oldItem));
|
||||||
|
queue.forEach(inbox => {
|
||||||
|
deliver(user, content, inbox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newId) {
|
||||||
|
const newItem = `${config.url}/notes/${newId}`;
|
||||||
|
const content = packAp(renderAdd(user, target, newItem));
|
||||||
|
queue.forEach(inbox => {
|
||||||
|
deliver(user, content, inbox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ローカルユーザーのリモートフォロワーのinboxリストを作成する
|
||||||
|
* @param user ローカルユーザー
|
||||||
|
*/
|
||||||
|
async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> {
|
||||||
|
const followers = await Following.find({
|
||||||
|
followeeId: user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
const queue: string[] = [];
|
||||||
|
|
||||||
|
followers.map(following => {
|
||||||
|
const follower = following._follower;
|
||||||
|
|
||||||
|
if (isRemoteUser(follower)) {
|
||||||
|
const inbox = follower.sharedInbox || follower.inbox;
|
||||||
|
if (!queue.includes(inbox)) queue.push(inbox);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return queue;
|
||||||
|
}
|
Reference in New Issue
Block a user