Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
a0c396a842 | |||
88fbc53e37 | |||
a2206b2d52 | |||
a95ff447d7 | |||
49dbd7f9d2 | |||
2ad2779096 | |||
23045369aa | |||
116faf26e6 | |||
2582b8d132 | |||
63f7941073 | |||
676f026085 | |||
2d6b20d34b | |||
99073b56df | |||
5dce81c0db | |||
be82d845a4 | |||
f49ccd0cd3 | |||
69d83f535d | |||
c7988fb6f5 | |||
3961fd08c9 | |||
e3faf64061 | |||
ed83993e15 | |||
0f8847bb74 | |||
a72cfa7535 | |||
514b74a19d | |||
a2c124306f | |||
273f67e268 |
@ -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: "閲覧注意"
|
||||||
|
@ -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",
|
||||||
|
@ -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[] = [];
|
||||||
|
@ -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);
|
||||||
|
@ -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': {
|
||||||
|
52
src/client/app/common/views/components/note-skeleton.vue
Normal file
52
src/client/app/common/views/components/note-skeleton.vue
Normal 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>
|
@ -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}`;
|
||||||
|
@ -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>
|
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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 }); }
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
368
src/client/app/desktop/views/components/ui.sidebar.vue
Normal file
368
src/client/app/desktop/views/components/ui.sidebar.vue
Normal 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>
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -56,6 +56,7 @@ const defaultDeviceSettings = {
|
|||||||
loadRawImages: false,
|
loadRawImages: false,
|
||||||
alwaysShowNsfw: false,
|
alwaysShowNsfw: false,
|
||||||
postStyle: 'standard',
|
postStyle: 'standard',
|
||||||
|
navbar: 'top',
|
||||||
mobileNotificationPosition: 'bottom'
|
mobileNotificationPosition: 'bottom'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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: {
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 : {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
16
test/mfm.ts
16
test/mfm.ts
@ -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);
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user