Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
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: "タスクマネージャ"
|
||||
third-parties: "サードパーティ"
|
||||
|
||||
navbar-position: "ナビゲーションバーの位置"
|
||||
navbar-position-top: "上"
|
||||
navbar-position-left: "左"
|
||||
navbar-position-right: "右"
|
||||
|
||||
desktop/views/components/settings.2fa.vue:
|
||||
intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
|
||||
detail: "詳細..."
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.13.0",
|
||||
"clientVersion": "1.0.10517",
|
||||
"version": "10.16.0",
|
||||
"clientVersion": "1.0.10532",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
@ -213,6 +213,7 @@
|
||||
"vue": "2.5.17",
|
||||
"vue-chartjs": "3.4.0",
|
||||
"vue-color": "2.7.0",
|
||||
"vue-content-loading": "1.5.3",
|
||||
"vue-cropperjs": "2.2.2",
|
||||
"vue-js-modal": "1.3.26",
|
||||
"vue-json-tree-view": "2.1.4",
|
||||
|
@ -9,7 +9,7 @@ import MiOS from '../../mios';
|
||||
*/
|
||||
export default class Stream extends EventEmitter {
|
||||
private stream: ReconnectingWebsocket;
|
||||
private state: string;
|
||||
public state: string;
|
||||
private sharedConnectionPools: Pool[] = [];
|
||||
private sharedConnections: SharedConnection[] = [];
|
||||
private nonSharedConnections: NonSharedConnection[] = [];
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import noteSkeleton from './note-skeleton.vue';
|
||||
import theme from './theme.vue';
|
||||
import instance from './instance.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 formRadio from './ui/form/radio.vue';
|
||||
|
||||
Vue.component('mk-note-skeleton', noteSkeleton);
|
||||
Vue.component('mk-theme', theme);
|
||||
Vue.component('mk-instance', instance);
|
||||
Vue.component('mk-cw-button', cwButton);
|
||||
|
@ -116,16 +116,16 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
case 'mention': {
|
||||
return (createElement as any)('a', {
|
||||
attrs: {
|
||||
href: `${url}/@${getAcct(token)}`,
|
||||
href: `${url}/${token.canonical}`,
|
||||
target: '_blank',
|
||||
dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
|
||||
style: 'color:var(--mfmMention);'
|
||||
},
|
||||
directives: [{
|
||||
name: 'user-preview',
|
||||
value: token.content
|
||||
value: token.canonical
|
||||
}]
|
||||
}, token.content);
|
||||
}, token.canonical);
|
||||
}
|
||||
|
||||
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 MkAutocomplete from '../components/autocomplete.vue';
|
||||
import renderAcct from '../../../../../misc/acct/render';
|
||||
import { toASCII } from 'punycode';
|
||||
|
||||
export default {
|
||||
bind(el, binding, vn) {
|
||||
@ -188,7 +188,7 @@ class Autocomplete {
|
||||
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
|
||||
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}`;
|
||||
|
@ -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>
|
@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue';
|
||||
import window from './window.vue';
|
||||
import noteFormWindow from './post-form-window.vue';
|
||||
import renoteFormWindow from './renote-form-window.vue';
|
||||
import ellipsisIcon from './ellipsis-icon.vue';
|
||||
import mediaImage from './media-image.vue';
|
||||
import mediaImageDialog from './media-image-dialog.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-post-form-window', noteFormWindow);
|
||||
Vue.component('mk-renote-form-window', renoteFormWindow);
|
||||
Vue.component('mk-ellipsis-icon', ellipsisIcon);
|
||||
Vue.component('mk-media-image', mediaImage);
|
||||
Vue.component('mk-media-image-dialog', mediaImageDialog);
|
||||
Vue.component('mk-media-video', mediaVideo);
|
||||
|
@ -9,6 +9,12 @@
|
||||
<button @click="resolveInitPromise">%i18n:@retry%</button>
|
||||
</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">
|
||||
<template v-for="(note, i) in _notes">
|
||||
@ -226,6 +232,10 @@ export default Vue.extend({
|
||||
> *
|
||||
transition transform .3s ease, opacity .3s ease
|
||||
|
||||
> .placeholder
|
||||
padding 32px
|
||||
opacity 0.3
|
||||
|
||||
> .notes
|
||||
> .date
|
||||
display block
|
||||
|
@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<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">
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<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%' }}
|
||||
</button>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@ -202,6 +207,10 @@ export default Vue.extend({
|
||||
> *
|
||||
transition transform .3s ease, opacity .3s ease
|
||||
|
||||
> .placeholder
|
||||
padding 16px
|
||||
opacity 0.3
|
||||
|
||||
> .notifications
|
||||
> div
|
||||
> .notification
|
||||
@ -319,13 +328,4 @@ export default Vue.extend({
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .loading
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
</style>
|
||||
|
@ -65,6 +65,7 @@ import { host } from '../../../config';
|
||||
import { erase, unique } from '../../../../../prelude/array';
|
||||
import { length } from 'stringz';
|
||||
import parseAcct from '../../../../../misc/acct/parse';
|
||||
import { toASCII } from 'punycode';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -158,14 +159,14 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
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) {
|
||||
const ast = parse(this.reply.text);
|
||||
|
||||
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;
|
||||
|
@ -88,6 +88,13 @@
|
||||
<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_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 class="web" v-show="page == 'web'">
|
||||
@ -293,6 +300,11 @@ export default Vue.extend({
|
||||
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: {
|
||||
get() { return this.$store.state.device.enableSounds; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
|
||||
|
@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="mk-timeline-core">
|
||||
<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">
|
||||
<p :class="$style.empty" slot="empty">
|
||||
@ -170,15 +167,10 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
|
||||
.mk-timeline-core
|
||||
> .mk-friends-maker
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> .fetching
|
||||
padding 64px 0
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="stylus" module>
|
||||
|
@ -17,8 +17,6 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
|
||||
.note
|
||||
display inline-block
|
||||
padding 8px
|
||||
|
237
src/client/app/desktop/views/components/ui.sidebar.vue
Normal file
237
src/client/app/desktop/views/components/ui.sidebar.vue
Normal file
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="header" :class="$store.state.device.navbar">
|
||||
<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>
|
||||
<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>
|
||||
<a @click="settings">%fa:cog%</a>
|
||||
</div>
|
||||
<div>
|
||||
<a @click="dark"><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></a>
|
||||
</div>
|
||||
<div class="signout">
|
||||
<a @click="signout">%fa:power-off%</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account">
|
||||
<router-link :to="`/@${ $store.state.i.username }`">
|
||||
<mk-avatar class="avatar" :user="$store.state.i"/>
|
||||
</router-link>
|
||||
</div>
|
||||
</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';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
hasGameInvitations: false,
|
||||
connection: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasUnreadMessagingMessage(): boolean {
|
||||
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
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%
|
||||
background var(--desktopHeaderBg)
|
||||
box-shadow var(--shadow)
|
||||
|
||||
&.left
|
||||
left 0
|
||||
|
||||
&.right
|
||||
right 0
|
||||
|
||||
> .nav
|
||||
> *
|
||||
> *
|
||||
display block
|
||||
width $width
|
||||
line-height 56px
|
||||
text-align center
|
||||
font-size 18px
|
||||
color var(--desktopHeaderFg)
|
||||
|
||||
&:hover
|
||||
color var(--desktopHeaderHoverFg)
|
||||
text-decoration none
|
||||
|
||||
> .nav.bottom
|
||||
position absolute
|
||||
bottom 64px
|
||||
left 0
|
||||
|
||||
> .post
|
||||
width $width
|
||||
height $width
|
||||
padding 10px
|
||||
|
||||
> button
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0 10px
|
||||
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 4px
|
||||
transition background 0.1s ease
|
||||
cursor pointer
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
|
||||
&:hover
|
||||
background var(--primaryLighten10) !important
|
||||
|
||||
&:active
|
||||
background var(--primaryDarken10) !important
|
||||
transition background 0s ease
|
||||
|
||||
> .account
|
||||
position absolute
|
||||
bottom 0
|
||||
left 0
|
||||
width $width
|
||||
height $width
|
||||
padding 12px
|
||||
|
||||
> *
|
||||
display block
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
> .avatar
|
||||
pointer-events none
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
</style>
|
@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div class="mk-ui" v-hotkey.global="keymap">
|
||||
<div class="bg" v-if="$store.getters.isSignedIn && $store.state.i.wallpaperUrl" :style="style"></div>
|
||||
<x-header class="header" v-show="!zenMode" ref="header"/>
|
||||
<div class="content">
|
||||
<x-header class="header" v-if="navbar == 'top'" v-show="!zenMode" ref="header"/>
|
||||
<x-sidebar class="sidebar" v-if="navbar != 'top'" ref="sidebar"/>
|
||||
<div class="content" :class="[{ sidebar: navbar != 'top' }, navbar]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<mk-stream-indicator v-if="$store.getters.isSignedIn"/>
|
||||
@ -12,10 +13,12 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XHeader from './ui.header.vue';
|
||||
import XSidebar from './ui.sidebar.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XHeader
|
||||
XHeader,
|
||||
XSidebar
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -25,6 +28,10 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
computed: {
|
||||
navbar(): string {
|
||||
return this.$store.state.device.navbar;
|
||||
},
|
||||
|
||||
style(): any {
|
||||
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
|
||||
return {
|
||||
@ -45,6 +52,12 @@ export default Vue.extend({
|
||||
watch: {
|
||||
'$store.state.uiHeaderHeight'() {
|
||||
this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
|
||||
},
|
||||
|
||||
navbar() {
|
||||
if (this.navbar != 'top') {
|
||||
this.$store.commit('setUiHeaderHeight', 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -87,4 +100,10 @@ export default Vue.extend({
|
||||
@media (max-width 1000px)
|
||||
display none
|
||||
|
||||
> .content.sidebar.left
|
||||
padding-left 64px
|
||||
|
||||
> .content.sidebar.right
|
||||
padding-right 64px
|
||||
|
||||
</style>
|
||||
|
@ -2,6 +2,12 @@
|
||||
<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu">
|
||||
<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">
|
||||
<p>%i18n:@error%</p>
|
||||
<button @click="resolveInitPromise">%i18n:@retry%</button>
|
||||
@ -205,6 +211,10 @@ export default Vue.extend({
|
||||
> *
|
||||
transition transform .3s ease, opacity .3s ease
|
||||
|
||||
> .placeholder
|
||||
padding 16px
|
||||
opacity 0.3
|
||||
|
||||
> .notes
|
||||
> .date
|
||||
display block
|
||||
|
@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<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">
|
||||
<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%' }}
|
||||
</button>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@ -161,6 +166,10 @@ export default Vue.extend({
|
||||
> *
|
||||
transition transform .3s ease, opacity .3s ease
|
||||
|
||||
> .placeholder
|
||||
padding 16px
|
||||
opacity 0.3
|
||||
|
||||
> .notifications
|
||||
|
||||
> .notification:not(:last-child)
|
||||
@ -207,13 +216,4 @@ export default Vue.extend({
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .loading
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
|
||||
<x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -3,9 +3,6 @@
|
||||
<header :class="$style.header">
|
||||
<h1>{{ q }}</h1>
|
||||
</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.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"/>
|
||||
@ -119,9 +116,6 @@ export default Vue.extend({
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
|
||||
.loading
|
||||
padding 64px 0
|
||||
|
||||
.empty
|
||||
display block
|
||||
margin 0 auto
|
||||
|
@ -3,9 +3,6 @@
|
||||
<header :class="$style.header">
|
||||
<h1>#{{ $route.params.tag }}</h1>
|
||||
</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>
|
||||
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
|
||||
</mk-ui>
|
||||
@ -108,9 +105,6 @@ export default Vue.extend({
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
|
||||
.loading
|
||||
padding 64px 0
|
||||
|
||||
.empty
|
||||
display block
|
||||
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-media'" @click="mode = 'with-media'">%fa:images% %i18n:@with-media%</span>
|
||||
</header>
|
||||
<div class="loading" v-if="fetching">
|
||||
<mk-ellipsis-icon/>
|
||||
</div>
|
||||
<mk-notes ref="timeline" :more="existMore ? more : null">
|
||||
<p class="empty" slot="empty">%fa:R comments%%i18n:@empty%</p>
|
||||
</mk-notes>
|
||||
@ -152,9 +149,6 @@ export default Vue.extend({
|
||||
&:hover
|
||||
color var(--desktopTimelineSrcHover)
|
||||
|
||||
> .loading
|
||||
padding 64px 0
|
||||
|
||||
> .empty
|
||||
display block
|
||||
margin 0 auto
|
||||
|
@ -443,7 +443,7 @@ export default class MiOS extends EventEmitter {
|
||||
};
|
||||
|
||||
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) {
|
||||
const id = Math.random().toString().substr(2, 8);
|
||||
|
@ -4,8 +4,10 @@
|
||||
|
||||
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
|
||||
|
||||
<div class="init" v-if="fetching">
|
||||
%fa:spinner .pulse%%i18n:common.loading%
|
||||
<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">
|
||||
@ -251,13 +253,12 @@ export default Vue.extend({
|
||||
[data-fa]
|
||||
margin-right 8px
|
||||
|
||||
> .init
|
||||
padding 64px 0
|
||||
text-align center
|
||||
color #999
|
||||
> .placeholder
|
||||
padding 16px
|
||||
opacity 0.3
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
@media (min-width 500px)
|
||||
padding 32px
|
||||
|
||||
> .empty
|
||||
margin 0 auto
|
||||
|
@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<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">
|
||||
<template v-for="(notification, i) in _notifications">
|
||||
@ -17,7 +23,6 @@
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@ -179,13 +184,11 @@ export default Vue.extend({
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
> .placeholder
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
opacity 0.3
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
@media (min-width 500px)
|
||||
padding 32px
|
||||
|
||||
</style>
|
||||
|
@ -62,6 +62,7 @@ import { host } from '../../../config';
|
||||
import { erase, unique } from '../../../../../prelude/array';
|
||||
import { length } from 'stringz';
|
||||
import parseAcct from '../../../../../misc/acct/parse';
|
||||
import { toASCII } from 'punycode';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -153,14 +154,14 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
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) {
|
||||
const ast = parse(this.reply.text);
|
||||
|
||||
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;
|
||||
|
@ -56,6 +56,7 @@ const defaultDeviceSettings = {
|
||||
loadRawImages: false,
|
||||
alwaysShowNsfw: false,
|
||||
postStyle: 'standard',
|
||||
navbar: 'top',
|
||||
mobileNotificationPosition: 'bottom'
|
||||
};
|
||||
|
||||
|
@ -2,10 +2,12 @@
|
||||
* Mention
|
||||
*/
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import { toUnicode } from 'punycode';
|
||||
|
||||
export type TextElementMention = {
|
||||
type: 'mention'
|
||||
content: string
|
||||
canonical: string
|
||||
username: string
|
||||
host: string
|
||||
};
|
||||
@ -15,9 +17,11 @@ export default function(text: string) {
|
||||
if (!match) return null;
|
||||
const mention = match[0];
|
||||
const { username, host } = parseAcct(mention.substr(1));
|
||||
const canonical = host != null ? `@${username}@${toUnicode(host)}` : mention;
|
||||
return {
|
||||
type: 'mention',
|
||||
content: mention,
|
||||
canonical,
|
||||
username,
|
||||
host
|
||||
} as TextElementMention;
|
||||
|
@ -100,8 +100,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
|
||||
}).then(notes => {
|
||||
notes.forEach(note => {
|
||||
note._files[note._files.findIndex(f => f._id.equals(file._id))] = file;
|
||||
Note.findOneAndUpdate({ _id: note._id }, {
|
||||
_files: note._files
|
||||
Note.update({ _id: note._id }, {
|
||||
$set: {
|
||||
_files: note._files
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -55,7 +55,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
recaptcha: config.recaptcha ? true : false,
|
||||
objectStorage: config.drive && config.drive.storage === 'minio',
|
||||
twitter: config.twitter ? true : false,
|
||||
serviceWorker: config.sw ? true : false
|
||||
serviceWorker: config.sw ? true : false,
|
||||
userRecommendation: config.user_recommendation ? config.user_recommendation : {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import config from '../../config';
|
||||
import * as mongo from 'mongodb';
|
||||
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 renderAdd from '../../remote/activitypub/renderer/add';
|
||||
import renderRemove from '../../remote/activitypub/renderer/remove';
|
||||
@ -27,11 +27,11 @@ export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
|
||||
let pinnedNoteIds = user.pinnedNoteIds || [];
|
||||
|
||||
//#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
|
||||
|
||||
if (pinnedNoteIds.length >= 5) {
|
||||
|
16
test/mfm.ts
16
test/mfm.ts
@ -8,9 +8,9 @@ describe('Text', () => {
|
||||
it('can be analyzed', () => {
|
||||
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
|
||||
assert.deepEqual([
|
||||
{ type: 'mention', content: '@himawari', username: 'himawari', host: null },
|
||||
{ type: 'mention', content: '@himawari', canonical: '@himawari', username: 'himawari', host: null },
|
||||
{ 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: 'emoji', content: ':cat:', emoji: 'cat'},
|
||||
{ type: 'text', content: ' '},
|
||||
@ -58,7 +58,7 @@ describe('Text', () => {
|
||||
it('local', () => {
|
||||
const tokens = analyze('@himawari お腹ペコい');
|
||||
assert.deepEqual([
|
||||
{ type: 'mention', content: '@himawari', username: 'himawari', host: null },
|
||||
{ type: 'mention', content: '@himawari', canonical: '@himawari', username: 'himawari', host: null },
|
||||
{ type: 'text', content: ' お腹ペコい' }
|
||||
], tokens);
|
||||
});
|
||||
@ -66,7 +66,15 @@ describe('Text', () => {
|
||||
it('remote', () => {
|
||||
const tokens = analyze('@hima_sub@namori.net お腹ペコい');
|
||||
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: ' お腹ペコい' }
|
||||
], tokens);
|
||||
});
|
||||
|
Reference in New Issue
Block a user