Refactor and usability improvements

This commit is contained in:
syuilo 2018-10-12 14:28:48 +09:00
parent 55fac36f2b
commit bb77b48efd
4 changed files with 299 additions and 341 deletions

View File

@ -0,0 +1,141 @@
import parse from '../../../../mfm/parse';
import { sum } from '../../../../prelude/array';
import MkNoteMenu from '..//views/components/note-menu.vue';
import MkReactionPicker from '../views/components/reaction-picker.vue';
function focus(el, fn) {
const target = fn(el);
if (target) {
if (target.hasAttribute('tabindex')) {
target.focus();
} else {
focus(target, fn);
}
}
}
type Opts = {
mobile?: boolean;
};
export default (opts: Opts = {}) => ({
data() {
return {
showContent: false
};
},
computed: {
keymap(): any {
return {
'r|left': () => this.reply(true),
'e|a|plus': () => this.react(true),
'q|right': () => this.renote(true),
'ctrl+q|ctrl+right': this.renoteDirectly,
'up|k|shift+tab': this.focusBefore,
'down|j|tab': this.focusAfter,
'esc': this.blur,
'm|o': () => this.menu(true),
's': this.toggleShowContent,
'1': () => this.reactDirectly('like'),
'2': () => this.reactDirectly('love'),
'3': () => this.reactDirectly('laugh'),
'4': () => this.reactDirectly('hmm'),
'5': () => this.reactDirectly('surprise'),
'6': () => this.reactDirectly('congrats'),
'7': () => this.reactDirectly('angry'),
'8': () => this.reactDirectly('confused'),
'9': () => this.reactDirectly('rip'),
'0': () => this.reactDirectly('pudding'),
};
},
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.fileIds.length == 0 &&
this.note.poll == null);
},
appearNote(): any {
return this.isRenote ? this.note.renote : this.note;
},
reactionsCount(): number {
return this.appearNote.reactionCounts
? sum(Object.values(this.appearNote.reactionCounts))
: 0;
},
title(): string {
return new Date(this.appearNote.createdAt).toLocaleString();
},
urls(): string[] {
if (this.appearNote.text) {
const ast = parse(this.appearNote.text);
return ast
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
.map(t => t.url);
} else {
return null;
}
}
},
methods: {
renoteDirectly() {
(this as any).api('notes/create', {
renoteId: this.appearNote.id
});
},
react(viaKeyboard = false) {
this.blur();
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.appearNote,
showFocus: viaKeyboard,
animation: !viaKeyboard,
compact: opts.mobile,
big: opts.mobile
}).$once('closed', this.focus);
},
reactDirectly(reaction) {
(this as any).api('notes/reactions/create', {
noteId: this.appearNote.id,
reaction: reaction
});
},
menu(viaKeyboard = false) {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.appearNote,
animation: !viaKeyboard,
compact: opts.mobile,
}).$once('closed', this.focus);
},
toggleShowContent() {
this.showContent = !this.showContent;
},
focus() {
this.$el.focus();
},
blur() {
this.$el.blur();
},
focusBefore() {
focus(this.$el, e => e.previousElementSibling);
},
focusAfter() {
focus(this.$el, e => e.nextElementSibling);
}
}
});

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="note" v-show="p.deletedAt == null" :tabindex="p.deletedAt == null ? '-1' : null" v-hotkey="keymap" :title="title"> <div class="note" v-show="appearNote.deletedAt == null" :tabindex="appearNote.deletedAt == null ? '-1' : null" v-hotkey="keymap" :title="title">
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> <div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="p.reply"/> <x-sub :note="appearNote.reply"/>
</div> </div>
<div class="renote" v-if="isRenote"> <div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/> <mk-avatar class="avatar" :user="note.user"/>
@ -12,90 +12,70 @@
<mk-time :time="note.createdAt"/> <mk-time :time="note.createdAt"/>
</div> </div>
<article> <article>
<mk-avatar class="avatar" :user="p.user"/> <mk-avatar class="avatar" :user="appearNote.user"/>
<div class="main"> <div class="main">
<mk-note-header class="header" :note="p"/> <mk-note-header class="header" :note="appearNote"/>
<div class="body"> <div class="body">
<p v-if="p.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> <span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="p.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">
<div class="text"> <div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span> <span v-if="appearNote.isHidden" style="opacity: 0.5">%i18n:@private%</span>
<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span> <a class="reply" v-if="appearNote.reply">%fa:reply%</a>
<a class="reply" v-if="p.reply">%fa:reply%</a> <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text"/>
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/> <a class="rp" v-if="appearNote.renote">RP:</a>
<a class="rp" v-if="p.renote">RP:</a>
</div> </div>
<div class="files" v-if="p.files.length > 0"> <div class="files" v-if="appearNote.files.length > 0">
<mk-media-list :media-list="p.files"/> <mk-media-list :media-list="appearNote.files"/>
</div> </div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
<div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/> <mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div> </div>
</div> </div>
<footer v-if="p.deletedAt == null"> <footer v-if="appearNote.deletedAt == null">
<mk-reactions-viewer :note="p" ref="reactionsViewer"/> <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
<button class="replyButton" @click="reply()" title="%i18n:@reply%"> <button class="replyButton" @click="reply()" title="%i18n:@reply%">
<template v-if="p.reply">%fa:reply-all%</template> <template v-if="appearNote.reply">%fa:reply-all%</template>
<template v-else>%fa:reply%</template> <template v-else>%fa:reply%</template>
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button> </button>
<button class="renoteButton" @click="renote()" title="%i18n:@renote%"> <button class="renoteButton" @click="renote()" title="%i18n:@renote%">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> %fa:retweet%<p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button> </button>
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%"> <button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> %fa:plus%<p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
</button> </button>
<button @click="menu()" ref="menuButton"> <button @click="menu()" ref="menuButton">
%fa:ellipsis-h% %fa:ellipsis-h%
</button> </button>
<!-- <button title="%i18n:@detail">
<template v-if="!isDetailOpened">%fa:caret-down%</template>
<template v-if="isDetailOpened">%fa:caret-up%</template>
</button> -->
</footer> </footer>
</div> </div>
</article> </article>
<div class="detail" v-if="isDetailOpened">
<mk-note-status-graph width="462" height="130" :note="p"/>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parse from '../../../../../mfm/parse';
import MkPostFormWindow from './post-form-window.vue'; import MkPostFormWindow from './post-form-window.vue';
import MkRenoteFormWindow from './renote-form-window.vue'; import MkRenoteFormWindow from './renote-form-window.vue';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue'; import XSub from './notes.note.sub.vue';
import { sum } from '../../../../../prelude/array'; import noteMixin from '../../../common/scripts/note-mixin';
import noteSubscriber from '../../../common/scripts/note-subscriber'; import noteSubscriber from '../../../common/scripts/note-subscriber';
function focus(el, fn) {
const target = fn(el);
if (target) {
if (target.hasAttribute('tabindex')) {
target.focus();
} else {
focus(target, fn);
}
}
}
export default Vue.extend({ export default Vue.extend({
components: { components: {
XSub XSub
}, },
mixins: [noteSubscriber('note')], mixins: [
noteMixin(),
noteSubscriber('note')
],
props: { props: {
note: { note: {
@ -104,136 +84,20 @@ export default Vue.extend({
} }
}, },
data() {
return {
showContent: false,
isDetailOpened: false
};
},
computed: {
keymap(): any {
return {
'r|left': () => this.reply(true),
'e|a|plus': () => this.react(true),
'q|right': () => this.renote(true),
'ctrl+q|ctrl+right': this.renoteDirectly,
'up|k|shift+tab': this.focusBefore,
'down|j|tab': this.focusAfter,
'esc': this.blur,
'm|o': () => this.menu(true),
's': this.toggleShowContent,
'1': () => this.reactDirectly('like'),
'2': () => this.reactDirectly('love'),
'3': () => this.reactDirectly('laugh'),
'4': () => this.reactDirectly('hmm'),
'5': () => this.reactDirectly('surprise'),
'6': () => this.reactDirectly('congrats'),
'7': () => this.reactDirectly('angry'),
'8': () => this.reactDirectly('confused'),
'9': () => this.reactDirectly('rip'),
'0': () => this.reactDirectly('pudding'),
};
},
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.fileIds.length == 0 &&
this.note.poll == null);
},
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
reactionsCount(): number {
return this.p.reactionCounts
? sum(Object.values(this.p.reactionCounts))
: 0;
},
title(): string {
return new Date(this.p.createdAt).toLocaleString();
},
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
return ast
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
.map(t => t.url);
} else {
return null;
}
}
},
methods: { methods: {
reply(viaKeyboard = false) { reply(viaKeyboard = false) {
(this as any).os.new(MkPostFormWindow, { (this as any).os.new(MkPostFormWindow, {
reply: this.p, reply: this.appearNote,
animation: !viaKeyboard animation: !viaKeyboard
}).$once('closed', this.focus); }).$once('closed', this.focus);
}, },
renote(viaKeyboard = false) { renote(viaKeyboard = false) {
(this as any).os.new(MkRenoteFormWindow, { (this as any).os.new(MkRenoteFormWindow, {
note: this.p, note: this.appearNote,
animation: !viaKeyboard animation: !viaKeyboard
}).$once('closed', this.focus); }).$once('closed', this.focus);
}, },
renoteDirectly() {
(this as any).api('notes/create', {
renoteId: this.p.id
});
},
react(viaKeyboard = false) {
this.blur();
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p,
showFocus: viaKeyboard,
animation: !viaKeyboard
}).$once('closed', this.focus);
},
reactDirectly(reaction) {
(this as any).api('notes/reactions/create', {
noteId: this.p.id,
reaction: reaction
});
},
menu(viaKeyboard = false) {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.p,
animation: !viaKeyboard
}).$once('closed', this.focus);
},
toggleShowContent() {
this.showContent = !this.showContent;
},
focus() {
this.$el.focus();
},
blur() {
this.$el.blur();
},
focusBefore() {
focus(this.$el, e => e.previousElementSibling);
},
focusAfter() {
focus(this.$el, e => e.nextElementSibling);
}
} }
}); });
</script> </script>
@ -445,10 +309,6 @@ export default Vue.extend({
&.reacted, &.reacted:hover &.reacted, &.reacted:hover
color var(--noteActionsReactionHover) color var(--noteActionsReactionHover)
> .detail
padding-top 4px
background rgba(#000, 0.0125)
</style> </style>
<style lang="stylus" module> <style lang="stylus" module>

View File

@ -1,7 +1,15 @@
<template> <template>
<div v-if="!mediaView" v-show="p.deletedAt == null" :tabindex="p.deletedAt == null ? '-1' : null" class="zyjjkidcqjnlegkqebitfviomuqmseqk" :class="{ renote: isRenote }"> <div
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> v-if="!mediaView"
<x-sub :note="p.reply"/> v-show="appearNote.deletedAt == null"
:tabindex="appearNote.deletedAt == null ? '-1' : null"
class="zyjjkidcqjnlegkqebitfviomuqmseqk"
:class="{ renote: isRenote }"
v-hotkey="keymap"
:title="title"
>
<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="appearNote.reply"/>
</div> </div>
<div class="renote" v-if="isRenote"> <div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/> <mk-avatar class="avatar" :user="note.user"/>
@ -12,43 +20,42 @@
<mk-time :time="note.createdAt"/> <mk-time :time="note.createdAt"/>
</div> </div>
<article> <article>
<mk-avatar class="avatar" :user="p.user"/> <mk-avatar class="avatar" :user="appearNote.user"/>
<div class="main"> <div class="main">
<mk-note-header class="header" :note="p" :mini="true"/> <mk-note-header class="header" :note="appearNote" :mini="true"/>
<div class="body"> <div class="body">
<p v-if="p.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> <span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="p.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">
<div class="text"> <div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> <span v-if="appearNote.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> <a class="reply" v-if="appearNote.reply">%fa:reply%</a>
<a class="reply" v-if="p.reply">%fa:reply%</a> <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i"/>
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> <a class="rp" v-if="appearNote.renote != null">RP:</a>
<a class="rp" v-if="p.renote != null">RP:</a>
</div> </div>
<div class="files" v-if="p.files.length > 0"> <div class="files" v-if="appearNote.files.length > 0">
<mk-media-list :media-list="p.files"/> <mk-media-list :media-list="appearNote.files"/>
</div> </div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="renote" v-if="p.renote"> <div class="renote" v-if="appearNote.renote">
<mk-note-preview :note="p.renote" :mini="true"/> <mk-note-preview :note="appearNote.renote" :mini="true"/>
</div> </div>
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="false" :mini="true"/> <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="false" :mini="true"/>
</div> </div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> <span class="app" v-if="appearNote.app">via <b>{{ appearNote.app.name }}</b></span>
</div> </div>
<footer> <footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/> <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
<button @click="reply"> <button @click="reply()">
<template v-if="p.reply">%fa:reply-all%</template> <template v-if="appearNote.reply">%fa:reply-all%</template>
<template v-else>%fa:reply%</template> <template v-else>%fa:reply%</template>
</button> </button>
<button @click="renote" title="Renote">%fa:retweet%</button> <button @click="renote()" title="Renote">%fa:retweet%</button>
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">%fa:plus%</button> <button :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton">%fa:plus%</button>
<button class="menu" @click="menu" ref="menuButton">%fa:ellipsis-h%</button> <button class="menu" @click="menu()" ref="menuButton">%fa:ellipsis-h%</button>
</footer> </footer>
</div> </div>
</article> </article>
@ -65,11 +72,10 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parse from '../../../../../../mfm/parse'; import MkPostFormWindow from '../../components/post-form-window.vue';
import MkRenoteFormWindow from '../../components/renote-form-window.vue';
import MkNoteMenu from '../../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue';
import XSub from './deck.note.sub.vue'; import XSub from './deck.note.sub.vue';
import noteMixin from '../../../../common/scripts/note-mixin';
import noteSubscriber from '../../../../common/scripts/note-subscriber'; import noteSubscriber from '../../../../common/scripts/note-subscriber';
export default Vue.extend({ export default Vue.extend({
@ -77,7 +83,10 @@ export default Vue.extend({
XSub XSub
}, },
mixins: [noteSubscriber('note')], mixins: [
noteMixin(),
noteSubscriber('note')
],
props: { props: {
note: { note: {
@ -91,64 +100,20 @@ export default Vue.extend({
} }
}, },
data() {
return {
showContent: false
};
},
computed: {
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.fileIds.length == 0 &&
this.note.poll == null);
},
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
return ast
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
.map(t => t.url);
} else {
return null;
}
}
},
methods: { methods: {
reply() { reply(viaKeyboard = false) {
(this as any).apis.post({ (this as any).os.new(MkPostFormWindow, {
reply: this.p reply: this.appearNote,
}); animation: !viaKeyboard
}).$once('closed', this.focus);
}, },
renote() { renote(viaKeyboard = false) {
(this as any).apis.post({ (this as any).os.new(MkRenoteFormWindow, {
renote: this.p note: this.appearNote,
}); animation: !viaKeyboard
}).$once('closed', this.focus);
}, },
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p,
compact: true
});
},
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.p,
compact: true
});
}
} }
}); });
</script> </script>
@ -168,6 +133,20 @@ export default Vue.extend({
font-size 13px font-size 13px
border-bottom solid 1px var(--faceDivider) border-bottom solid 1px var(--faceDivider)
&:focus
z-index 1
&:after
content ""
pointer-events none
position absolute
top 2px
right 2px
bottom 2px
left 2px
border 2px solid var(--primaryAlpha03)
border-radius 4px
&:last-of-type &:last-of-type
border-bottom none border-bottom none

View File

@ -1,7 +1,13 @@
<template> <template>
<div class="note" v-show="p.deletedAt == null" :tabindex="p.deletedAt == null ? '-1' : null" :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }"> <div
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)"> class="note"
<x-sub :note="p.reply"/> v-show="appearNote.deletedAt == null"
:tabindex="appearNote.deletedAt == null ? '-1' : null"
:class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }"
v-hotkey="keymap"
>
<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="appearNote.reply"/>
</div> </div>
<div class="renote" v-if="isRenote"> <div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/> <mk-avatar class="avatar" :user="note.user"/>
@ -12,47 +18,45 @@
<mk-time :time="note.createdAt"/> <mk-time :time="note.createdAt"/>
</div> </div>
<article> <article>
<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/> <mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main"> <div class="main">
<mk-note-header class="header" :note="p" :mini="true"/> <mk-note-header class="header" :note="appearNote" :mini="true"/>
<div class="body"> <div class="body">
<p v-if="p.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> <span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="p.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">
<div class="text"> <div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span> <span v-if="appearNote.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span> <a class="reply" v-if="appearNote.reply">%fa:reply%</a>
<a class="reply" v-if="p.reply">%fa:reply%</a> <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text"/>
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/> <a class="rp" v-if="appearNote.renote != null">RP:</a>
<a class="rp" v-if="p.renote != null">RP:</a>
</div> </div>
<div class="files" v-if="p.files.length > 0"> <div class="files" v-if="appearNote.files.length > 0">
<mk-media-list :media-list="p.files"/> <mk-media-list :media-list="appearNote.files"/>
</div> </div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/> <mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
<div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
</div> </div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> <span class="app" v-if="appearNote.app">via <b>{{ appearNote.apappearNote.name }}</b></span>
</div> </div>
<footer v-if="p.deletedAt == null"> <footer v-if="appearNote.deletedAt == null">
<mk-reactions-viewer :note="p" ref="reactionsViewer"/> <mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
<button @click="reply"> <button @click="reply()">
<template v-if="p.reply">%fa:reply-all%</template> <template v-if="appearNote.reply">%fa:reply-all%</template>
<template v-else>%fa:reply%</template> <template v-else>%fa:reply%</template>
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button> </button>
<button @click="renote" title="Renote"> <button @click="renote()" title="Renote">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> %fa:retweet%<p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button> </button>
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton"> <button :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton">
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> %fa:plus%<p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
</button> </button>
<button class="menu" @click="menu" ref="menuButton"> <button class="menu" @click="menu()" ref="menuButton">
%fa:ellipsis-h% %fa:ellipsis-h%
</button> </button>
</footer> </footer>
@ -63,12 +67,9 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue'; import XSub from './note.sub.vue';
import { sum } from '../../../../../prelude/array'; import noteMixin from '../../../common/scripts/note-mixin';
import noteSubscriber from '../../../common/scripts/note-subscriber'; import noteSubscriber from '../../../common/scripts/note-subscriber';
export default Vue.extend({ export default Vue.extend({
@ -76,9 +77,19 @@ export default Vue.extend({
XSub XSub
}, },
mixins: [noteSubscriber('note')], mixins: [
noteMixin({
mobile: true
}),
noteSubscriber('note')
],
props: ['note'], props: {
note: {
type: Object,
required: true
}
},
data() { data() {
return { return {
@ -86,65 +97,18 @@ export default Vue.extend({
}; };
}, },
computed: {
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.fileIds.length == 0 &&
this.note.poll == null);
},
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
reactionsCount(): number {
return this.p.reactionCounts
? sum(Object.values(this.p.reactionCounts))
: 0;
},
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
return ast
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
.map(t => t.url);
} else {
return null;
}
}
},
methods: { methods: {
reply() { reply() {
(this as any).apis.post({ (this as any).apis.post({
reply: this.p reply: this.appearNote
}); });
}, },
renote() { renote() {
(this as any).apis.post({ (this as any).apis.post({
renote: this.p renote: this.appearNote
}); });
}, },
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p,
compact: true,
big: true
});
},
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.p,
compact: true
});
}
} }
}); });
</script> </script>
@ -154,6 +118,20 @@ export default Vue.extend({
font-size 12px font-size 12px
border-bottom solid 1px var(--faceDivider) border-bottom solid 1px var(--faceDivider)
&:focus
z-index 1
&:after
content ""
pointer-events none
position absolute
top 2px
right 2px
bottom 2px
left 2px
border 2px solid var(--primaryAlpha03)
border-radius 4px
&:last-of-type &:last-of-type
border-bottom none border-bottom none