Compare commits

...

26 Commits

Author SHA1 Message Date
a0c396a842 10.19.0 2018-10-15 18:03:28 +09:00
88fbc53e37 Resolve #2314 2018-10-15 18:02:57 +09:00
a2206b2d52 🎨 2018-10-15 17:55:59 +09:00
a95ff447d7 🎨 2018-10-15 17:43:25 +09:00
49dbd7f9d2 Fix following from Preroma does not complete (#2905)
* In Follow Accept/Reject, send previous received id

* In Follow Accept/Reject, send Activity.actor
2018-10-15 16:51:22 +09:00
2ad2779096 10.18.0 2018-10-15 06:03:50 +09:00
23045369aa 🎨 2018-10-15 06:03:15 +09:00
116faf26e6 10.17.0 2018-10-15 05:29:58 +09:00
2582b8d132 🎨 2018-10-15 05:28:35 +09:00
63f7941073 🎨 2018-10-15 05:18:39 +09:00
676f026085 🎨 2018-10-15 04:36:31 +09:00
2d6b20d34b 10.16.0 2018-10-14 19:45:51 +09:00
99073b56df Resolve #2900 2018-10-14 19:44:30 +09:00
5dce81c0db 非ASCIIなドメインへのメンションの修正 (#2903)
* punycodeでされたmentionのラベルをunicodeとして表示する

* post-form mentionはpunycodeにする

* mentionの表示はURLもAPI向けもunicodeにする
2018-10-14 16:56:19 +09:00
be82d845a4 expose user recommendation config in /api/meta (#2902) 2018-10-14 16:54:09 +09:00
f49ccd0cd3 10.15.0 2018-10-14 10:17:04 +09:00
69d83f535d Clean up 2018-10-14 10:16:07 +09:00
c7988fb6f5 🎨 2018-10-14 10:16:02 +09:00
3961fd08c9 Fix #2901 2018-10-14 10:06:10 +09:00
e3faf64061 10.14.0 2018-10-14 09:49:16 +09:00
ed83993e15 Fix 2018-10-14 09:48:47 +09:00
0f8847bb74 Resolve #2618 2018-10-14 09:47:38 +09:00
a72cfa7535 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-10-14 06:46:49 +09:00
514b74a19d Clean up 2018-10-14 06:44:20 +09:00
a2c124306f Update mios.ts 2018-10-14 05:26:36 +09:00
273f67e268 Fix bug 2018-10-13 23:12:48 +09:00
48 changed files with 680 additions and 164 deletions

View File

@ -883,6 +883,11 @@ desktop/views/components/settings.vue:
task-manager: "タスクマネージャ" task-manager: "タスクマネージャ"
third-parties: "サードパーティ" third-parties: "サードパーティ"
navbar-position: "ナビゲーションバーの位置"
navbar-position-top: "上"
navbar-position-left: "左"
navbar-position-right: "右"
desktop/views/components/settings.2fa.vue: desktop/views/components/settings.2fa.vue:
intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。" intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
detail: "詳細..." detail: "詳細..."
@ -1234,6 +1239,8 @@ mobile/views/components/drive.file-detail.vue:
hash: "ハッシュ (md5)" hash: "ハッシュ (md5)"
exif: "EXIF" exif: "EXIF"
nsfw: "閲覧注意" nsfw: "閲覧注意"
mark-as-sensitive: "閲覧注意に設定"
unmark-as-sensitive: "閲覧注意を解除"
mobile/views/components/media-image.vue: mobile/views/components/media-image.vue:
sensitive: "閲覧注意" sensitive: "閲覧注意"

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "10.13.0", "version": "10.19.0",
"clientVersion": "1.0.10517", "clientVersion": "1.0.10543",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@ -213,6 +213,7 @@
"vue": "2.5.17", "vue": "2.5.17",
"vue-chartjs": "3.4.0", "vue-chartjs": "3.4.0",
"vue-color": "2.7.0", "vue-color": "2.7.0",
"vue-content-loading": "1.5.3",
"vue-cropperjs": "2.2.2", "vue-cropperjs": "2.2.2",
"vue-js-modal": "1.3.26", "vue-js-modal": "1.3.26",
"vue-json-tree-view": "2.1.4", "vue-json-tree-view": "2.1.4",

View File

@ -9,7 +9,7 @@ import MiOS from '../../mios';
*/ */
export default class Stream extends EventEmitter { export default class Stream extends EventEmitter {
private stream: ReconnectingWebsocket; private stream: ReconnectingWebsocket;
private state: string; public state: string;
private sharedConnectionPools: Pool[] = []; private sharedConnectionPools: Pool[] = [];
private sharedConnections: SharedConnection[] = []; private sharedConnections: SharedConnection[] = [];
private nonSharedConnections: NonSharedConnection[] = []; private nonSharedConnections: NonSharedConnection[] = [];

View File

@ -1,5 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import noteSkeleton from './note-skeleton.vue';
import theme from './theme.vue'; import theme from './theme.vue';
import instance from './instance.vue'; import instance from './instance.vue';
import cwButton from './cw-button.vue'; import cwButton from './cw-button.vue';
@ -44,6 +45,7 @@ import uiSelect from './ui/select.vue';
import formButton from './ui/form/button.vue'; import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue'; import formRadio from './ui/form/radio.vue';
Vue.component('mk-note-skeleton', noteSkeleton);
Vue.component('mk-theme', theme); Vue.component('mk-theme', theme);
Vue.component('mk-instance', instance); Vue.component('mk-instance', instance);
Vue.component('mk-cw-button', cwButton); Vue.component('mk-cw-button', cwButton);

View File

@ -116,16 +116,16 @@ export default Vue.component('misskey-flavored-markdown', {
case 'mention': { case 'mention': {
return (createElement as any)('a', { return (createElement as any)('a', {
attrs: { attrs: {
href: `${url}/@${getAcct(token)}`, href: `${url}/${token.canonical}`,
target: '_blank', target: '_blank',
dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token), dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
style: 'color:var(--mfmMention);' style: 'color:var(--mfmMention);'
}, },
directives: [{ directives: [{
name: 'user-preview', name: 'user-preview',
value: token.content value: token.canonical
}] }]
}, token.content); }, token.canonical);
} }
case 'hashtag': { case 'hashtag': {

View File

@ -0,0 +1,52 @@
<template>
<div>
<vue-content-loading v-if="width" :width="width" :height="100" :primary="primary" :secondary="secondary">
<circle cx="30" cy="30" r="30" />
<rect x="75" y="13" rx="4" ry="4" :width="150 + r1" height="15" />
<rect x="75" y="39" rx="4" ry="4" :width="260 + r2" height="10" />
<rect x="75" y="59" rx="4" ry="4" :width="230 + r3" height="10" />
</vue-content-loading>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import VueContentLoading from 'vue-content-loading';
import * as tinycolor from 'tinycolor2';
export default Vue.extend({
components: {
VueContentLoading,
},
data() {
return {
width: 0,
r1: (Math.random() * 100) - 50,
r2: (Math.random() * 100) - 50,
r3: (Math.random() * 100) - 50
};
},
computed: {
text(): tinycolor.Instance {
const text = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text'));
return text;
},
primary(): string {
return '#' + this.text.clone().toHex();
},
secondary(): string {
return '#' + this.text.clone().darken(20).toHex();
}
},
mounted() {
let width = this.$el.clientWidth;
if (width < 400) width = 400;
this.width = width;
}
});
</script>

View File

@ -1,6 +1,6 @@
import * as getCaretCoordinates from 'textarea-caret'; import * as getCaretCoordinates from 'textarea-caret';
import MkAutocomplete from '../components/autocomplete.vue'; import MkAutocomplete from '../components/autocomplete.vue';
import renderAcct from '../../../../../misc/acct/render'; import { toASCII } from 'punycode';
export default { export default {
bind(el, binding, vn) { bind(el, binding, vn) {
@ -188,7 +188,7 @@ class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf('@')); const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
const after = source.substr(caret); const after = source.substr(caret);
const acct = renderAcct(value); const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
// 挿入 // 挿入
this.text = `${trimmedBefore}@${acct} ${after}`; this.text = `${trimmedBefore}@${acct} ${after}`;

View File

@ -1,37 +0,0 @@
<template>
<div class="mk-ellipsis-icon">
<div></div><div></div><div></div>
</div>
</template>
<style lang="stylus" scoped>
.mk-ellipsis-icon
width 70px
margin 0 auto
text-align center
> div
display inline-block
width 18px
height 18px
background-color rgba(#000, 0.3)
border-radius 100%
animation bounce 1.4s infinite ease-in-out both
&:nth-child(1)
animation-delay 0s
&:nth-child(2)
margin 0 6px
animation-delay 0.16s
&:nth-child(3)
animation-delay 0.32s
@keyframes bounce
0%, 80%, 100%
transform scale(0)
40%
transform scale(1)
</style>

View File

@ -38,7 +38,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="main"> <div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }">
<template v-if="customize"> <template v-if="customize">
<x-draggable v-for="place in ['left', 'right']" <x-draggable v-for="place in ['left', 'right']"
:list="widgets[place]" :list="widgets[place]"
@ -359,12 +359,10 @@ export default Vue.extend({
box-shadow var(--shadow) box-shadow var(--shadow)
border-radius var(--round) border-radius var(--round)
@media (max-width 700px) &.side
padding 0 > .main
width calc(100% - 280px)
> .tl max-width 680px
border none
border-radius 0
> *:not(.main) > *:not(.main)
width 280px width 280px
@ -381,12 +379,22 @@ export default Vue.extend({
padding-right 16px padding-right 16px
order 3 order 3
@media (max-width 1100px) &.side
@media (max-width 1000px)
> *:not(.main)
display none
> .main
width 100%
max-width 700px
margin 0 auto
&:not(.side)
@media (max-width 1200px)
> *:not(.main) > *:not(.main)
display none display none
> .main > .main
float none
width 100% width 100%
max-width 700px max-width 700px
margin 0 auto margin 0 auto

View File

@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue';
import window from './window.vue'; import window from './window.vue';
import noteFormWindow from './post-form-window.vue'; import noteFormWindow from './post-form-window.vue';
import renoteFormWindow from './renote-form-window.vue'; import renoteFormWindow from './renote-form-window.vue';
import ellipsisIcon from './ellipsis-icon.vue';
import mediaImage from './media-image.vue'; import mediaImage from './media-image.vue';
import mediaImageDialog from './media-image-dialog.vue'; import mediaImageDialog from './media-image-dialog.vue';
import mediaVideo from './media-video.vue'; import mediaVideo from './media-video.vue';
@ -39,7 +38,6 @@ Vue.component('mk-sub-note-content', subNoteContent);
Vue.component('mk-window', window); Vue.component('mk-window', window);
Vue.component('mk-post-form-window', noteFormWindow); Vue.component('mk-post-form-window', noteFormWindow);
Vue.component('mk-renote-form-window', renoteFormWindow); Vue.component('mk-renote-form-window', renoteFormWindow);
Vue.component('mk-ellipsis-icon', ellipsisIcon);
Vue.component('mk-media-image', mediaImage); Vue.component('mk-media-image', mediaImage);
Vue.component('mk-media-image-dialog', mediaImageDialog); Vue.component('mk-media-image-dialog', mediaImageDialog);
Vue.component('mk-media-video', mediaVideo); Vue.component('mk-media-video', mediaVideo);

View File

@ -9,6 +9,12 @@
<button @click="resolveInitPromise">%i18n:@retry%</button> <button @click="resolveInitPromise">%i18n:@retry%</button>
</div> </div>
<div class="placeholder" v-if="fetching">
<template v-for="i in 10">
<mk-note-skeleton :key="i"/>
</template>
</div>
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes"> <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">
@ -226,6 +232,10 @@ export default Vue.extend({
> * > *
transition transform .3s ease, opacity .3s ease transition transform .3s ease, opacity .3s ease
> .placeholder
padding 32px
opacity 0.3
> .notes > .notes
> .date > .date
display block display block

View File

@ -1,5 +1,11 @@
<template> <template>
<div class="mk-notifications"> <div class="mk-notifications">
<div class="placeholder" v-if="fetching">
<template v-for="i in 10">
<mk-note-skeleton :key="i"/>
</template>
</div>
<div class="notifications" v-if="notifications.length != 0"> <div class="notifications" v-if="notifications.length != 0">
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
@ -102,7 +108,6 @@
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button> </button>
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
</div> </div>
</template> </template>
@ -202,6 +207,10 @@ export default Vue.extend({
> * > *
transition transform .3s ease, opacity .3s ease transition transform .3s ease, opacity .3s ease
> .placeholder
padding 16px
opacity 0.3
> .notifications > .notifications
> div > div
> .notification > .notification
@ -319,13 +328,4 @@ export default Vue.extend({
text-align center text-align center
color #aaa color #aaa
> .loading
margin 0
padding 16px
text-align center
color #aaa
> [data-fa]
margin-right 4px
</style> </style>

View File

@ -65,6 +65,7 @@ import { host } from '../../../config';
import { erase, unique } from '../../../../../prelude/array'; import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz'; import { length } from 'stringz';
import parseAcct from '../../../../../misc/acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
import { toASCII } from 'punycode';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -158,14 +159,14 @@ export default Vue.extend({
} }
if (this.reply && this.reply.user.host != null) { if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${this.reply.user.host} `; this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
} }
if (this.reply && this.reply.text != null) { if (this.reply && this.reply.text != null) {
const ast = parse(this.reply.text); const ast = parse(this.reply.text);
ast.filter(t => t.type == 'mention').forEach(x => { ast.filter(t => t.type == 'mention').forEach(x => {
const mention = x.host ? `@${x.username}@${x.host}` : `@${x.username}`; const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
// 自分は除外 // 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) return; if (this.$store.state.i.username == x.username && x.host == null) return;

View File

@ -88,6 +88,13 @@
<ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch> <ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
<ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch> <ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
<ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch> <ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
<section>
<header>%i18n:@navbar-position%</header>
<ui-radio v-model="navbar" value="top">%i18n:@navbar-position-top%</ui-radio>
<ui-radio v-model="navbar" value="left">%i18n:@navbar-position-left%</ui-radio>
<ui-radio v-model="navbar" value="right">%i18n:@navbar-position-right%</ui-radio>
</section>
</section> </section>
<section class="web" v-show="page == 'web'"> <section class="web" v-show="page == 'web'">
@ -293,6 +300,11 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); } set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
}, },
navbar: {
get() { return this.$store.state.device.navbar; },
set(value) { this.$store.commit('device/set', { key: 'navbar', value }); }
},
enableSounds: { enableSounds: {
get() { return this.$store.state.device.enableSounds; }, get() { return this.$store.state.device.enableSounds; },
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }

View File

@ -1,9 +1,6 @@
<template> <template>
<div class="mk-timeline-core"> <div class="mk-timeline-core">
<mk-friends-maker v-if="src == 'home' && alone"/> <mk-friends-maker v-if="src == 'home' && alone"/>
<div class="fetching" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<mk-notes ref="timeline" :more="existMore ? more : null"> <mk-notes ref="timeline" :more="existMore ? more : null">
<p :class="$style.empty" slot="empty"> <p :class="$style.empty" slot="empty">
@ -170,15 +167,10 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.mk-timeline-core .mk-timeline-core
> .mk-friends-maker > .mk-friends-maker
border-bottom solid 1px #eee border-bottom solid 1px #eee
> .fetching
padding 64px 0
</style> </style>
<style lang="stylus" module> <style lang="stylus" module>

View File

@ -157,6 +157,9 @@ export default Vue.extend({
font-family Meiryo, sans-serif font-family Meiryo, sans-serif
text-decoration none text-decoration none
@media (max-width 1100px)
display none
[data-fa] [data-fa]
margin-left 8px margin-left 8px
@ -171,6 +174,9 @@ export default Vue.extend({
border-radius 4px border-radius 4px
transition filter 100ms ease transition filter 100ms ease
@media (max-width 1100px)
margin-left 8px
> .menu > .menu
$bgcolor = var(--face) $bgcolor = var(--face)
display block display block

View File

@ -17,8 +17,6 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.note .note
display inline-block display inline-block
padding 8px padding 8px

View File

@ -29,6 +29,9 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.search .search
@media (max-width 800px)
display none !important
> [data-fa] > [data-fa]
display block display block
position absolute position absolute
@ -58,6 +61,9 @@ export default Vue.extend({
transition color 0.5s ease, border 0.5s ease transition color 0.5s ease, border 0.5s ease
color var(--desktopHeaderSearchFg) color var(--desktopHeaderSearchFg)
@media (max-width 1000px)
width 10em
&::placeholder &::placeholder
color var(--desktopHeaderFg) color var(--desktopHeaderFg)

View File

@ -0,0 +1,368 @@
<template>
<div class="header" :class="navbar">
<div class="body">
<div class="post">
<button @click="post" title="%i18n:@post%">%fa:pencil-alt%</button>
</div>
<div class="nav" v-if="$store.getters.isSignedIn">
<div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop">
<router-link to="/">%fa:home%</router-link>
</div>
<div class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
<router-link to="/deck">%fa:columns%</router-link>
</div>
<div class="messaging">
<a @click="messaging">%fa:comments%<template v-if="hasUnreadMessagingMessage">%fa:circle%</template></a>
</div>
<div class="game">
<a @click="game">%fa:gamepad%<template v-if="hasGameInvitations">%fa:circle%</template></a>
</div>
</div>
<div class="nav bottom" v-if="$store.getters.isSignedIn">
<div>
<a @click="drive">%fa:cloud%</a>
</div>
<div ref="notificationsButton" :class="{ active: showNotifications }">
<a @click="notifications">%fa:R bell%</a>
</div>
<div>
<a @click="settings">%fa:cog%</a>
</div>
</div>
<div class="account">
<router-link :to="`/@${ $store.state.i.username }`">
<mk-avatar class="avatar" :user="$store.state.i"/>
</router-link>
<div class="nav menu">
<div class="signout">
<a @click="signout">%fa:power-off%</a>
</div>
<div>
<router-link to="/i/favorites">%fa:star%</router-link>
</div>
<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
<a @click="followRequests">%fa:envelope R%<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a>
</div>
</div>
</div>
<div class="nav dark">
<div>
<a @click="dark"><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></a>
</div>
</div>
</div>
<transition :name="`slide-${navbar}`">
<div class="notifications" v-if="showNotifications" ref="notifications" :class="navbar">
<mk-notifications/>
</div>
</transition>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import MkUserListsWindow from './user-lists-window.vue';
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
import MkMessagingWindow from './messaging-window.vue';
import MkGameWindow from './game-window.vue';
import contains from '../../../common/scripts/contains';
export default Vue.extend({
data() {
return {
hasGameInvitations: false,
connection: null,
showNotifications: false
};
},
computed: {
hasUnreadMessagingMessage(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
},
navbar(): string {
return this.$store.state.device.navbar;
},
},
mounted() {
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.useSharedConnection('main');
this.connection.on('reversiInvited', this.onReversiInvited);
this.connection.on('reversi_no_invites', this.onReversiNoInvites);
}
},
beforeDestroy() {
if (this.$store.getters.isSignedIn) {
this.connection.dispose();
}
},
methods: {
onReversiInvited() {
this.hasGameInvitations = true;
},
onReversiNoInvites() {
this.hasGameInvitations = false;
},
messaging() {
(this as any).os.new(MkMessagingWindow);
},
game() {
(this as any).os.new(MkGameWindow);
},
post() {
(this as any).apis.post();
},
drive() {
(this as any).os.new(MkDriveWindow);
},
list() {
const w = (this as any).os.new(MkUserListsWindow);
w.$once('choosen', list => {
this.$router.push(`i/lists/${ list.id }`);
});
},
followRequests() {
(this as any).os.new(MkFollowRequestsWindow);
},
settings() {
(this as any).os.new(MkSettingsWindow);
},
signout() {
(this as any).os.signout();
},
notifications() {
this.showNotifications ? this.closeNotifications() : this.openNotifications();
},
openNotifications() {
this.showNotifications = true;
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.addEventListener('mousedown', this.onMousedown);
});
},
closeNotifications() {
this.showNotifications = false;
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.removeEventListener('mousedown', this.onMousedown);
});
},
onMousedown(e) {
e.preventDefault();
if (
!contains(this.$refs.notifications, e.target) &&
this.$refs.notifications != e.target &&
!contains(this.$refs.notificationsButton, e.target) &&
this.$refs.notificationsButton != e.target
) {
this.closeNotifications();
}
return false;
},
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
value: !this.$store.state.device.darkmode
});
},
goToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
}
});
</script>
<style lang="stylus" scoped>
.header
$width = 68px
position fixed
top 0
z-index 1000
width $width
height 100%
&.left
left 0
box-shadow var(--shadowRight)
&.right
right 0
box-shadow var(--shadowLeft)
> .body
position fixed
top 0
z-index 1
width $width
height 100%
background var(--desktopHeaderBg)
> .post
width $width
height $width
padding 12px
> button
display inline-block
margin 0
padding 0
height 100%
width 100%
font-size 1.2em
font-weight normal
text-decoration none
color var(--primaryForeground)
background var(--primary) !important
outline none
border none
border-radius 100%
transition background 0.1s ease
cursor pointer
*
pointer-events none
&:hover
background var(--primaryLighten10) !important
&:active
background var(--primaryDarken10) !important
transition background 0s ease
> .nav.bottom
position absolute
bottom 128px
left 0
> .account
position absolute
bottom 64px
left 0
width $width
height $width
padding 14px
> .menu
display none
position absolute
bottom 64px
left 0
background var(--desktopHeaderBg)
&:hover
> .menu
display block
> *:not(.menu)
display block
width 100%
height 100%
> .avatar
pointer-events none
width 100%
height 100%
> .dark
position absolute
bottom 0
left 0
width $width
height $width
> .notifications
position fixed
top 0
width 350px
height 100%
overflow auto
background var(--face)
&.left
left $width
box-shadow var(--shadowRight)
&.right
right $width
box-shadow var(--shadowLeft)
.nav
> *
> *
display block
width $width
line-height 52px
text-align center
font-size 18px
color var(--desktopHeaderFg)
&:hover
background rgba(0, 0, 0, 0.05)
color var(--desktopHeaderHoverFg)
text-decoration none
&:active
background rgba(0, 0, 0, 0.1)
&.left
.nav
> *
&.active
box-shadow -4px 0 var(--primary) inset
&.right
.nav
> *
&.active
box-shadow 4px 0 var(--primary) inset
.slide-left-enter-active,
.slide-left-leave-active {
transition: all 0.2s ease;
}
.slide-left-enter, .slide-left-leave-to {
transform: translateX(-16px);
opacity: 0;
}
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 0.2s ease;
}
.slide-right-enter, .slide-right-leave-to {
transform: translateX(16px);
opacity: 0;
}
</style>

View File

@ -1,8 +1,9 @@
<template> <template>
<div class="mk-ui" v-hotkey.global="keymap"> <div class="mk-ui" v-hotkey.global="keymap">
<div class="bg" v-if="$store.getters.isSignedIn && $store.state.i.wallpaperUrl" :style="style"></div> <div class="bg" v-if="$store.getters.isSignedIn && $store.state.i.wallpaperUrl" :style="style"></div>
<x-header class="header" v-show="!zenMode" ref="header"/> <x-header class="header" v-if="navbar == 'top'" v-show="!zenMode" ref="header"/>
<div class="content"> <x-sidebar class="sidebar" v-if="navbar != 'top'" ref="sidebar"/>
<div class="content" :class="[{ sidebar: navbar != 'top' }, navbar]">
<slot></slot> <slot></slot>
</div> </div>
<mk-stream-indicator v-if="$store.getters.isSignedIn"/> <mk-stream-indicator v-if="$store.getters.isSignedIn"/>
@ -12,10 +13,12 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import XHeader from './ui.header.vue'; import XHeader from './ui.header.vue';
import XSidebar from './ui.sidebar.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XHeader XHeader,
XSidebar
}, },
data() { data() {
@ -25,6 +28,10 @@ export default Vue.extend({
}, },
computed: { computed: {
navbar(): string {
return this.$store.state.device.navbar;
},
style(): any { style(): any {
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {}; if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
return { return {
@ -45,6 +52,12 @@ export default Vue.extend({
watch: { watch: {
'$store.state.uiHeaderHeight'() { '$store.state.uiHeaderHeight'() {
this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
},
navbar() {
if (this.navbar != 'top') {
this.$store.commit('setUiHeaderHeight', 0);
}
} }
}, },
@ -83,8 +96,10 @@ export default Vue.extend({
background-attachment fixed background-attachment fixed
opacity 0.3 opacity 0.3
> .header > .content.sidebar.left
@media (max-width 1000px) padding-left 68px
display none
> .content.sidebar.right
padding-right 68px
</style> </style>

View File

@ -276,13 +276,24 @@ export default Vue.extend({
min-width 330px min-width 330px
height 100% height 100%
background var(--face) background var(--face)
border-radius 6px border-radius var(--round)
//box-shadow 0 2px 16px rgba(#000, 0.1) box-shadow var(--shadow)
overflow hidden overflow hidden
&.draghover &.draghover
box-shadow 0 0 0 2px var(--primaryAlpha08) box-shadow 0 0 0 2px var(--primaryAlpha08)
&:after
content ""
display block
position absolute
z-index 1000
top 0
left 0
width 100%
height 100%
background var(--primaryAlpha02)
&.dragging &.dragging
box-shadow 0 0 0 2px var(--primaryAlpha04) box-shadow 0 0 0 2px var(--primaryAlpha04)
@ -338,6 +349,7 @@ export default Vue.extend({
> .toggleActive > .toggleActive
> .menu > .menu
padding 0
width $header-height width $header-height
line-height $header-height line-height $header-height
font-size 16px font-size 16px

View File

@ -2,6 +2,12 @@
<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu"> <div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu">
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
<div class="placeholder" v-if="fetching">
<template v-for="i in 10">
<mk-note-skeleton :key="i"/>
</template>
</div>
<div v-if="!fetching && requestInitPromise != null"> <div v-if="!fetching && requestInitPromise != null">
<p>%i18n:@error%</p> <p>%i18n:@error%</p>
<button @click="resolveInitPromise">%i18n:@retry%</button> <button @click="resolveInitPromise">%i18n:@retry%</button>
@ -205,6 +211,10 @@ export default Vue.extend({
> * > *
transition transform .3s ease, opacity .3s ease transition transform .3s ease, opacity .3s ease
> .placeholder
padding 16px
opacity 0.3
> .notes > .notes
> .date > .date
display block display block

View File

@ -1,5 +1,11 @@
<template> <template>
<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl"> <div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
<div class="placeholder" v-if="fetching">
<template v-for="i in 10">
<mk-note-skeleton :key="i"/>
</template>
</div>
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications"> <template v-for="(notification, i) in _notifications">
@ -14,7 +20,6 @@
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button> </button>
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
</div> </div>
</template> </template>
@ -161,6 +166,10 @@ export default Vue.extend({
> * > *
transition transform .3s ease, opacity .3s ease transition transform .3s ease, opacity .3s ease
> .placeholder
padding 16px
opacity 0.3
> .notifications > .notifications
> .notification:not(:last-child) > .notification:not(:last-child)
@ -207,13 +216,4 @@ export default Vue.extend({
text-align center text-align center
color #aaa color #aaa
> .loading
margin 0
padding 16px
text-align center
color #aaa
> [data-fa]
margin-right 4px
</style> </style>

View File

@ -3,9 +3,6 @@
<header :class="$style.header"> <header :class="$style.header">
<h1>{{ q }}</h1> <h1>{{ q }}</h1>
</header> </header>
<div :class="$style.loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<p :class="$style.notAvailable" v-if="!fetching && notAvailable">%i18n:@not-available%</p> <p :class="$style.notAvailable" v-if="!fetching && notAvailable">%i18n:@not-available%</p>
<p :class="$style.empty" v-if="!fetching && empty">%fa:search% {{ '%i18n:not-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:not-found%'.split('{}')[1] }}</p> <p :class="$style.empty" v-if="!fetching && empty">%fa:search% {{ '%i18n:not-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:not-found%'.split('{}')[1] }}</p>
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/> <mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
@ -119,9 +116,6 @@ export default Vue.extend({
border-radius 6px border-radius 6px
overflow hidden overflow hidden
.loading
padding 64px 0
.empty .empty
display block display block
margin 0 auto margin 0 auto

View File

@ -3,9 +3,6 @@
<header :class="$style.header"> <header :class="$style.header">
<h1>#{{ $route.params.tag }}</h1> <h1>#{{ $route.params.tag }}</h1>
</header> </header>
<div :class="$style.loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<p :class="$style.empty" v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p> <p :class="$style.empty" v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/> <mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
</mk-ui> </mk-ui>
@ -108,9 +105,6 @@ export default Vue.extend({
border-radius 6px border-radius 6px
overflow hidden overflow hidden
.loading
padding 64px 0
.empty .empty
display block display block
margin 0 auto margin 0 auto

View File

@ -5,9 +5,6 @@
<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%fa:comments% %i18n:@with-replies%</span> <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%fa:comments% %i18n:@with-replies%</span>
<span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%fa:images% %i18n:@with-media%</span> <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%fa:images% %i18n:@with-media%</span>
</header> </header>
<div class="loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<mk-notes ref="timeline" :more="existMore ? more : null"> <mk-notes ref="timeline" :more="existMore ? more : null">
<p class="empty" slot="empty">%fa:R comments%%i18n:@empty%</p> <p class="empty" slot="empty">%fa:R comments%%i18n:@empty%</p>
</mk-notes> </mk-notes>
@ -152,9 +149,6 @@ export default Vue.extend({
&:hover &:hover
color var(--desktopTimelineSrcHover) color var(--desktopTimelineSrcHover)
> .loading
padding 64px 0
> .empty > .empty
display block display block
margin 0 auto margin 0 auto

View File

@ -124,11 +124,17 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
//#region shadow //#region shadow
const shadow = '0 3px 8px rgba(0, 0, 0, 0.2)'; const shadow = '0 3px 8px rgba(0, 0, 0, 0.2)';
const shadowRight = '4px 0 4px rgba(0, 0, 0, 0.1)';
const shadowLeft = '-4px 0 4px rgba(0, 0, 0, 0.1)';
if (os.store.state.settings.useShadow) document.documentElement.style.setProperty('--shadow', shadow); if (os.store.state.settings.useShadow) document.documentElement.style.setProperty('--shadow', shadow);
if (os.store.state.settings.useShadow) document.documentElement.style.setProperty('--shadowRight', shadowRight);
if (os.store.state.settings.useShadow) document.documentElement.style.setProperty('--shadowLeft', shadowLeft);
os.store.watch(s => { os.store.watch(s => {
return s.settings.useShadow; return s.settings.useShadow;
}, v => { }, v => {
document.documentElement.style.setProperty('--shadow', v ? shadow : 'none'); document.documentElement.style.setProperty('--shadow', v ? shadow : 'none');
document.documentElement.style.setProperty('--shadowRight', v ? shadowRight : 'none');
document.documentElement.style.setProperty('--shadowLeft', v ? shadowLeft : 'none');
}); });
//#endregion //#endregion

View File

@ -443,7 +443,7 @@ export default class MiOS extends EventEmitter {
}; };
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
const viaStream = this.stream && this.store.state.device.apiViaStream && !forceFetch; const viaStream = this.stream && this.stream.state == 'connected' && this.store.state.device.apiViaStream && !forceFetch;
if (viaStream) { if (viaStream) {
const id = Math.random().toString().substr(2, 8); const id = Math.random().toString().substr(2, 8);

View File

@ -41,6 +41,8 @@
<ui-button link :href="`${file.url}?download`" :download="file.name">%fa:download% %i18n:@download%</ui-button> <ui-button link :href="`${file.url}?download`" :download="file.name">%fa:download% %i18n:@download%</ui-button>
<ui-button @click="rename">%fa:pencil-alt% %i18n:@rename%</ui-button> <ui-button @click="rename">%fa:pencil-alt% %i18n:@rename%</ui-button>
<ui-button @click="move">%fa:R folder-open% %i18n:@move%</ui-button> <ui-button @click="move">%fa:R folder-open% %i18n:@move%</ui-button>
<ui-button @click="toggleSensitive" v-if="file.isSensitive">%fa:R eye% %i18n:@unmark-as-sensitive%</ui-button>
<ui-button @click="toggleSensitive" v-else>%fa:R eye-slash% %i18n:@mark-as-sensitive%</ui-button>
<ui-button @click="del">%fa:trash-alt R% %i18n:@delete%</ui-button> <ui-button @click="del">%fa:trash-alt R% %i18n:@delete%</ui-button>
</div> </div>
</div> </div>
@ -71,25 +73,30 @@ import { gcd } from '../../../../../prelude/math';
export default Vue.extend({ export default Vue.extend({
props: ['file'], props: ['file'],
data() { data() {
return { return {
gcd, gcd,
exif: null exif: null
}; };
}, },
computed: { computed: {
browser(): any { browser(): any {
return this.$parent; return this.$parent;
}, },
kind(): string { kind(): string {
return this.file.type.split('/')[0]; return this.file.type.split('/')[0];
}, },
style(): any { style(): any {
return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? { return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? {
'background-color': `rgb(${ this.file.properties.avgColor.join(',') })` 'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
} : {}; } : {};
} }
}, },
methods: { methods: {
rename() { rename() {
const name = window.prompt('%i18n:@rename%', this.file.name); const name = window.prompt('%i18n:@rename%', this.file.name);
@ -101,6 +108,7 @@ export default Vue.extend({
this.browser.cf(this.file, true); this.browser.cf(this.file, true);
}); });
}, },
move() { move() {
(this as any).apis.chooseDriveFolder().then(folder => { (this as any).apis.chooseDriveFolder().then(folder => {
(this as any).api('drive/files/update', { (this as any).api('drive/files/update', {
@ -111,6 +119,7 @@ export default Vue.extend({
}); });
}); });
}, },
del() { del() {
(this as any).api('drive/files/delete', { (this as any).api('drive/files/delete', {
fileId: this.file.id fileId: this.file.id
@ -118,9 +127,20 @@ export default Vue.extend({
this.browser.cd(this.file.folderId, true); this.browser.cd(this.file.folderId, true);
}); });
}, },
toggleSensitive() {
(this as any).api('drive/files/update', {
fileId: this.file.id,
isSensitive: !this.file.isSensitive
});
this.file.isSensitive = !this.file.isSensitive;
},
showCreatedAt() { showCreatedAt() {
alert(new Date(this.file.createdAt).toLocaleString()); alert(new Date(this.file.createdAt).toLocaleString());
}, },
onImageLoaded() { onImageLoaded() {
const self = this; const self = this;
EXIF.getData(this.$refs.img, function(this: any) { EXIF.getData(this.$refs.img, function(this: any) {

View File

@ -4,8 +4,10 @@
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
<div class="init" v-if="fetching"> <div class="placeholder" v-if="fetching">
%fa:spinner .pulse%%i18n:common.loading% <template v-for="i in 10">
<mk-note-skeleton :key="i"/>
</template>
</div> </div>
<div v-if="!fetching && requestInitPromise != null"> <div v-if="!fetching && requestInitPromise != null">
@ -251,13 +253,12 @@ export default Vue.extend({
[data-fa] [data-fa]
margin-right 8px margin-right 8px
> .init > .placeholder
padding 64px 0 padding 16px
text-align center opacity 0.3
color #999
> [data-fa] @media (min-width 500px)
margin-right 4px padding 32px
> .empty > .empty
margin 0 auto margin 0 auto

View File

@ -1,5 +1,11 @@
<template> <template>
<div class="mk-notifications"> <div class="mk-notifications">
<div class="placeholder" v-if="fetching">
<template v-for="i in 10">
<mk-note-skeleton :key="i"/>
</template>
</div>
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications"> <template v-for="(notification, i) in _notifications">
@ -17,7 +23,6 @@
</button> </button>
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
</div> </div>
</template> </template>
@ -179,13 +184,11 @@ export default Vue.extend({
text-align center text-align center
color #aaa color #aaa
> .fetching > .placeholder
margin 0
padding 16px padding 16px
text-align center opacity 0.3
color #aaa
> [data-fa] @media (min-width 500px)
margin-right 4px padding 32px
</style> </style>

View File

@ -62,6 +62,7 @@ import { host } from '../../../config';
import { erase, unique } from '../../../../../prelude/array'; import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz'; import { length } from 'stringz';
import parseAcct from '../../../../../misc/acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
import { toASCII } from 'punycode';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -153,14 +154,14 @@ export default Vue.extend({
} }
if (this.reply && this.reply.user.host != null) { if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${this.reply.user.host} `; this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
} }
if (this.reply && this.reply.text != null) { if (this.reply && this.reply.text != null) {
const ast = parse(this.reply.text); const ast = parse(this.reply.text);
ast.filter(t => t.type == 'mention').forEach(x => { ast.filter(t => t.type == 'mention').forEach(x => {
const mention = x.host ? `@${x.username}@${x.host}` : `@${x.username}`; const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
// 自分は除外 // 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) return; if (this.$store.state.i.username == x.username && x.host == null) return;

View File

@ -56,6 +56,7 @@ const defaultDeviceSettings = {
loadRawImages: false, loadRawImages: false,
alwaysShowNsfw: false, alwaysShowNsfw: false,
postStyle: 'standard', postStyle: 'standard',
navbar: 'top',
mobileNotificationPosition: 'bottom' mobileNotificationPosition: 'bottom'
}; };

View File

@ -2,10 +2,12 @@
* Mention * Mention
*/ */
import parseAcct from '../../../misc/acct/parse'; import parseAcct from '../../../misc/acct/parse';
import { toUnicode } from 'punycode';
export type TextElementMention = { export type TextElementMention = {
type: 'mention' type: 'mention'
content: string content: string
canonical: string
username: string username: string
host: string host: string
}; };
@ -15,9 +17,11 @@ export default function(text: string) {
if (!match) return null; if (!match) return null;
const mention = match[0]; const mention = match[0];
const { username, host } = parseAcct(mention.substr(1)); const { username, host } = parseAcct(mention.substr(1));
const canonical = host != null ? `@${username}@${toUnicode(host)}` : mention;
return { return {
type: 'mention', type: 'mention',
content: mention, content: mention,
canonical,
username, username,
host host
} as TextElementMention; } as TextElementMention;

View File

@ -12,6 +12,7 @@ export type IFollowRequest = {
createdAt: Date; createdAt: Date;
followeeId: mongo.ObjectID; followeeId: mongo.ObjectID;
followerId: mongo.ObjectID; followerId: mongo.ObjectID;
requestId?: string; // id of Follow Activity
// 非正規化 // 非正規化
_followee: { _followee: {

View File

@ -23,5 +23,5 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
throw new Error('フォローしようとしているユーザーはローカルユーザーではありません'); throw new Error('フォローしようとしているユーザーはローカルユーザーではありません');
} }
await follow(actor, followee); await follow(actor, followee, activity.id);
}; };

View File

@ -1,4 +1,8 @@
export default (object: any) => ({ import config from '../../../config';
import { ILocalUser } from '../../../models/user';
export default (object: any, user: ILocalUser) => ({
type: 'Accept', type: 'Accept',
actor: `${config.url}/users/${user._id}`,
object object
}); });

View File

@ -1,8 +1,14 @@
import config from '../../../config'; import config from '../../../config';
import { IUser, isLocalUser } from '../../../models/user'; import { IUser, isLocalUser } from '../../../models/user';
export default (follower: IUser, followee: IUser) => ({ export default (follower: IUser, followee: IUser, requestId?: string) => {
const follow = {
type: 'Follow', type: 'Follow',
actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri, actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri,
object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri
}); } as any;
if (requestId) follow.id = requestId;
return follow;
};

View File

@ -1,4 +1,8 @@
export default (object: any) => ({ import config from '../../../config';
import { ILocalUser } from '../../../models/user';
export default (object: any, user: ILocalUser) => ({
type: 'Reject', type: 'Reject',
actor: `${config.url}/users/${user._id}`,
object object
}); });

View File

@ -100,8 +100,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}).then(notes => { }).then(notes => {
notes.forEach(note => { notes.forEach(note => {
note._files[note._files.findIndex(f => f._id.equals(file._id))] = file; note._files[note._files.findIndex(f => f._id.equals(file._id))] = file;
Note.findOneAndUpdate({ _id: note._id }, { Note.update({ _id: note._id }, {
$set: {
_files: note._files _files: note._files
}
}); });
}); });
}); });

View File

@ -55,7 +55,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
recaptcha: config.recaptcha ? true : false, recaptcha: config.recaptcha ? true : false,
objectStorage: config.drive && config.drive.storage === 'minio', objectStorage: config.drive && config.drive.storage === 'minio',
twitter: config.twitter ? true : false, twitter: config.twitter ? true : false,
serviceWorker: config.sw ? true : false serviceWorker: config.sw ? true : false,
userRecommendation: config.user_recommendation ? config.user_recommendation : {}
} }
}); });
}); });

View File

@ -10,13 +10,13 @@ import renderAccept from '../../remote/activitypub/renderer/accept';
import { deliver } from '../../queue'; import { deliver } from '../../queue';
import createFollowRequest from './requests/create'; import createFollowRequest from './requests/create';
export default async function(follower: IUser, followee: IUser) { export default async function(follower: IUser, followee: IUser, requestId?: string) {
// フォロー対象が鍵アカウントである or // フォロー対象が鍵アカウントである or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if (followee.isLocked || (followee.carefulBot && follower.isBot) || (isLocalUser(follower) && isRemoteUser(followee))) { if (followee.isLocked || (followee.carefulBot && follower.isBot) || (isLocalUser(follower) && isRemoteUser(followee))) {
await createFollowRequest(follower, followee); await createFollowRequest(follower, followee, requestId);
return; return;
} }
@ -79,7 +79,7 @@ export default async function(follower: IUser, followee: IUser) {
} }
if (isRemoteUser(follower) && isLocalUser(followee)) { if (isRemoteUser(follower) && isLocalUser(followee)) {
const content = pack(renderAccept(renderFollow(follower, followee))); const content = pack(renderAccept(renderFollow(follower, followee, requestId), followee));
deliver(followee, content, follower.inbox); deliver(followee, content, follower.inbox);
} }
} }

View File

@ -29,7 +29,12 @@ export default async function(followee: IUser, follower: IUser) {
}); });
if (isRemoteUser(follower)) { if (isRemoteUser(follower)) {
const content = pack(renderAccept(renderFollow(follower, followee))); const request = await FollowRequest.findOne({
followeeId: followee._id,
followerId: follower._id
});
const content = pack(renderAccept(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
deliver(followee as ILocalUser, content, follower.inbox); deliver(followee as ILocalUser, content, follower.inbox);
} }

View File

@ -6,11 +6,12 @@ import renderFollow from '../../../remote/activitypub/renderer/follow';
import { deliver } from '../../../queue'; import { deliver } from '../../../queue';
import FollowRequest from '../../../models/follow-request'; import FollowRequest from '../../../models/follow-request';
export default async function(follower: IUser, followee: IUser) { export default async function(follower: IUser, followee: IUser, requestId?: string) {
await FollowRequest.insert({ await FollowRequest.insert({
createdAt: new Date(), createdAt: new Date(),
followerId: follower._id, followerId: follower._id,
followeeId: followee._id, followeeId: followee._id,
requestId,
// 非正規化 // 非正規化
_follower: { _follower: {

View File

@ -8,7 +8,12 @@ import { publishMainStream } from '../../../stream';
export default async function(followee: IUser, follower: IUser) { export default async function(followee: IUser, follower: IUser) {
if (isRemoteUser(follower)) { if (isRemoteUser(follower)) {
const content = pack(renderReject(renderFollow(follower, followee))); const request = await FollowRequest.findOne({
followeeId: followee._id,
followerId: follower._id
});
const content = pack(renderReject(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
deliver(followee as ILocalUser, content, follower.inbox); deliver(followee as ILocalUser, content, follower.inbox);
} }

View File

@ -1,7 +1,7 @@
import config from '../../config'; import config from '../../config';
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user'; import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user';
import Note from '../../models/note'; import Note, { packMany } from '../../models/note';
import Following from '../../models/following'; import Following from '../../models/following';
import renderAdd from '../../remote/activitypub/renderer/add'; import renderAdd from '../../remote/activitypub/renderer/add';
import renderRemove from '../../remote/activitypub/renderer/remove'; import renderRemove from '../../remote/activitypub/renderer/remove';
@ -27,11 +27,11 @@ export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
let pinnedNoteIds = user.pinnedNoteIds || []; let pinnedNoteIds = user.pinnedNoteIds || [];
//#region 現在ピン留め投稿している投稿が実際にデータベースに存在しているのかチェック //#region 現在ピン留め投稿している投稿が実際にデータベースに存在しているのかチェック
// データベースの欠損などで存在していない場合があるので。 // データベースの欠損などで存在していない(または破損している)場合があるので。
// 存在していなかったらピン留め投稿から外す // 存在していなかったらピン留め投稿から外す
const pinnedNotes = (await Promise.all(pinnedNoteIds.map(id => Note.findOne({ _id: id })))).filter(x => x != null); const pinnedNotes = await packMany(pinnedNoteIds, null, { detail: true });
pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n._id.equals(id))); pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n.id.toString() === id.toHexString()));
//#endregion //#endregion
if (pinnedNoteIds.length >= 5) { if (pinnedNoteIds.length >= 5) {

View File

@ -8,9 +8,9 @@ describe('Text', () => {
it('can be analyzed', () => { it('can be analyzed', () => {
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr'); const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
assert.deepEqual([ assert.deepEqual([
{ type: 'mention', content: '@himawari', username: 'himawari', host: null }, { type: 'mention', content: '@himawari', canonical: '@himawari', username: 'himawari', host: null },
{ type: 'text', content: ' '}, { type: 'text', content: ' '},
{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }, { type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
{ type: 'text', content: ' お腹ペコい ' }, { type: 'text', content: ' お腹ペコい ' },
{ type: 'emoji', content: ':cat:', emoji: 'cat'}, { type: 'emoji', content: ':cat:', emoji: 'cat'},
{ type: 'text', content: ' '}, { type: 'text', content: ' '},
@ -58,7 +58,7 @@ describe('Text', () => {
it('local', () => { it('local', () => {
const tokens = analyze('@himawari お腹ペコい'); const tokens = analyze('@himawari お腹ペコい');
assert.deepEqual([ assert.deepEqual([
{ type: 'mention', content: '@himawari', username: 'himawari', host: null }, { type: 'mention', content: '@himawari', canonical: '@himawari', username: 'himawari', host: null },
{ type: 'text', content: ' お腹ペコい' } { type: 'text', content: ' お腹ペコい' }
], tokens); ], tokens);
}); });
@ -66,7 +66,15 @@ describe('Text', () => {
it('remote', () => { it('remote', () => {
const tokens = analyze('@hima_sub@namori.net お腹ペコい'); const tokens = analyze('@hima_sub@namori.net お腹ペコい');
assert.deepEqual([ assert.deepEqual([
{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }, { type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
{ type: 'text', content: ' お腹ペコい' }
], tokens);
});
it('remote punycode', () => {
const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah お腹ペコい');
assert.deepEqual([
{ type: 'mention', content: '@hima_sub@xn--q9j5bya.xn--zckzah', canonical: '@hima_sub@なもり.テスト', username: 'hima_sub', host: 'xn--q9j5bya.xn--zckzah' },
{ type: 'text', content: ' お腹ペコい' } { type: 'text', content: ' お腹ペコい' }
], tokens); ], tokens);
}); });