@@ -29,7 +29,7 @@ import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['data'],
|
||||
created() {
|
||||
this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
|
||||
this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
|
||||
const peak = Math.max.apply(null, this.data.map(d => d.total));
|
||||
|
||||
let x = 0;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown">
|
||||
<title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
|
||||
<title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title>
|
||||
<polyline
|
||||
:points="pointsPost"
|
||||
:points="pointsNote"
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#41ddde"/>
|
||||
@@ -12,7 +12,7 @@
|
||||
stroke-width="1"
|
||||
stroke="#f7796c"/>
|
||||
<polyline
|
||||
:points="pointsRepost"
|
||||
:points="pointsRenote"
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#a1de41"/>
|
||||
@@ -48,24 +48,24 @@ export default Vue.extend({
|
||||
viewBoxY: 60,
|
||||
zoom: 1,
|
||||
pos: 0,
|
||||
pointsPost: null,
|
||||
pointsNote: null,
|
||||
pointsReply: null,
|
||||
pointsRepost: null,
|
||||
pointsRenote: null,
|
||||
pointsTotal: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.data.reverse();
|
||||
this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
|
||||
this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
|
||||
this.render();
|
||||
},
|
||||
methods: {
|
||||
render() {
|
||||
const peak = Math.max.apply(null, this.data.map(d => d.total));
|
||||
if (peak != 0) {
|
||||
this.pointsPost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsNote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsRepost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsRenote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,23 +4,23 @@ import ui from './ui.vue';
|
||||
import uiNotification from './ui-notification.vue';
|
||||
import home from './home.vue';
|
||||
import timeline from './timeline.vue';
|
||||
import posts from './posts.vue';
|
||||
import subPostContent from './sub-post-content.vue';
|
||||
import notes from './notes.vue';
|
||||
import subNoteContent from './sub-note-content.vue';
|
||||
import window from './window.vue';
|
||||
import postFormWindow from './post-form-window.vue';
|
||||
import repostFormWindow from './repost-form-window.vue';
|
||||
import noteFormWindow from './post-form-window.vue';
|
||||
import renoteFormWindow from './renote-form-window.vue';
|
||||
import analogClock from './analog-clock.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';
|
||||
import notifications from './notifications.vue';
|
||||
import postForm from './post-form.vue';
|
||||
import repostForm from './repost-form.vue';
|
||||
import noteForm from './post-form.vue';
|
||||
import renoteForm from './renote-form.vue';
|
||||
import followButton from './follow-button.vue';
|
||||
import postPreview from './post-preview.vue';
|
||||
import notePreview from './note-preview.vue';
|
||||
import drive from './drive.vue';
|
||||
import postDetail from './post-detail.vue';
|
||||
import noteDetail from './note-detail.vue';
|
||||
import settings from './settings.vue';
|
||||
import calendar from './calendar.vue';
|
||||
import activity from './activity.vue';
|
||||
@@ -34,23 +34,23 @@ Vue.component('mk-ui', ui);
|
||||
Vue.component('mk-ui-notification', uiNotification);
|
||||
Vue.component('mk-home', home);
|
||||
Vue.component('mk-timeline', timeline);
|
||||
Vue.component('mk-posts', posts);
|
||||
Vue.component('mk-sub-post-content', subPostContent);
|
||||
Vue.component('mk-notes', notes);
|
||||
Vue.component('mk-sub-note-content', subNoteContent);
|
||||
Vue.component('mk-window', window);
|
||||
Vue.component('mk-post-form-window', postFormWindow);
|
||||
Vue.component('mk-repost-form-window', repostFormWindow);
|
||||
Vue.component('mk-post-form-window', noteFormWindow);
|
||||
Vue.component('mk-renote-form-window', renoteFormWindow);
|
||||
Vue.component('mk-analog-clock', analogClock);
|
||||
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);
|
||||
Vue.component('mk-notifications', notifications);
|
||||
Vue.component('mk-post-form', postForm);
|
||||
Vue.component('mk-repost-form', repostForm);
|
||||
Vue.component('mk-post-form', noteForm);
|
||||
Vue.component('mk-renote-form', renoteForm);
|
||||
Vue.component('mk-follow-button', followButton);
|
||||
Vue.component('mk-post-preview', postPreview);
|
||||
Vue.component('mk-note-preview', notePreview);
|
||||
Vue.component('mk-drive', drive);
|
||||
Vue.component('mk-post-detail', postDetail);
|
||||
Vue.component('mk-note-detail', noteDetail);
|
||||
Vue.component('mk-settings', settings);
|
||||
Vue.component('mk-calendar', calendar);
|
||||
Vue.component('mk-activity', activity);
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<div class="fetching" v-if="fetching">
|
||||
<mk-ellipsis-icon/>
|
||||
</div>
|
||||
<p class="empty" v-if="posts.length == 0 && !fetching">
|
||||
<p class="empty" v-if="notes.length == 0 && !fetching">
|
||||
%fa:R comments%
|
||||
<span v-if="mode == 'all'">あなた宛ての投稿はありません。</span>
|
||||
<span v-if="mode == 'following'">あなたがフォローしているユーザーからの言及はありません。</span>
|
||||
</p>
|
||||
<mk-posts :posts="posts" ref="timeline"/>
|
||||
<mk-notes :notes="notes" ref="timeline"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default Vue.extend({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
mode: 'all',
|
||||
posts: []
|
||||
notes: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -56,23 +56,23 @@ export default Vue.extend({
|
||||
},
|
||||
fetch(cb?) {
|
||||
this.fetching = true;
|
||||
this.posts = [];
|
||||
(this as any).api('posts/mentions', {
|
||||
this.notes = [];
|
||||
(this as any).api('notes/mentions', {
|
||||
following: this.mode == 'following'
|
||||
}).then(posts => {
|
||||
this.posts = posts;
|
||||
}).then(notes => {
|
||||
this.notes = notes;
|
||||
this.fetching = false;
|
||||
if (cb) cb();
|
||||
});
|
||||
},
|
||||
more() {
|
||||
if (this.moreFetching || this.fetching || this.posts.length == 0) return;
|
||||
if (this.moreFetching || this.fetching || this.notes.length == 0) return;
|
||||
this.moreFetching = true;
|
||||
(this as any).api('posts/mentions', {
|
||||
(this as any).api('notes/mentions', {
|
||||
following: this.mode == 'following',
|
||||
untilId: this.posts[this.posts.length - 1].id
|
||||
}).then(posts => {
|
||||
this.posts = this.posts.concat(posts);
|
||||
untilId: this.notes[this.notes.length - 1].id
|
||||
}).then(notes => {
|
||||
this.notes = this.notes.concat(notes);
|
||||
this.moreFetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<div class="sub" :title="title">
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`">
|
||||
<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
|
||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
|
||||
</router-link>
|
||||
<div class="main">
|
||||
<header>
|
||||
<div class="left">
|
||||
<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</router-link>
|
||||
<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</router-link>
|
||||
<span class="username">@{{ acct }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<router-link class="time" :to="`/@${acct}/${post.id}`">
|
||||
<mk-time :time="post.createdAt"/>
|
||||
<router-link class="time" :to="`/@${acct}/${note.id}`">
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</router-link>
|
||||
</div>
|
||||
</header>
|
||||
<div class="body">
|
||||
<mk-post-html v-if="post.text" :text="post.text" :i="os.i" :class="$style.text"/>
|
||||
<div class="media" v-if="post.media > 0">
|
||||
<mk-media-list :media-list="post.media"/>
|
||||
<mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/>
|
||||
<div class="media" v-if="note.media > 0">
|
||||
<mk-media-list :media-list="note.media"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,16 +32,16 @@ import getAcct from '../../../../../acct/render';
|
||||
import getUserName from '../../../../../renderers/get-user-name';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
props: ['note'],
|
||||
computed: {
|
||||
acct() {
|
||||
return getAcct(this.post.user);
|
||||
return getAcct(this.note.user);
|
||||
},
|
||||
name() {
|
||||
return getUserName(this.post.user);
|
||||
return getUserName(this.note.user);
|
||||
},
|
||||
title(): string {
|
||||
return dateStringify(this.post.createdAt);
|
||||
return dateStringify(this.note.createdAt);
|
||||
}
|
||||
}
|
||||
});
|
||||
448
src/client/app/desktop/views/components/note-detail.vue
Normal file
448
src/client/app/desktop/views/components/note-detail.vue
Normal file
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div class="mk-note-detail" :title="title">
|
||||
<button
|
||||
class="read-more"
|
||||
v-if="p.reply && p.reply.replyId && context == null"
|
||||
title="会話をもっと読み込む"
|
||||
@click="fetchContext"
|
||||
:disabled="contextFetching"
|
||||
>
|
||||
<template v-if="!contextFetching">%fa:ellipsis-v%</template>
|
||||
<template v-if="contextFetching">%fa:spinner .pulse%</template>
|
||||
</button>
|
||||
<div class="context">
|
||||
<x-sub v-for="note in context" :key="note.id" :note="note"/>
|
||||
</div>
|
||||
<div class="reply-to" v-if="p.reply">
|
||||
<x-sub :note="p.reply"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<p>
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
|
||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
|
||||
</router-link>
|
||||
%fa:retweet%
|
||||
<router-link class="name" :href="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
|
||||
がRenote
|
||||
</p>
|
||||
</div>
|
||||
<article>
|
||||
<router-link class="avatar-anchor" :to="`/@${pAcct}`">
|
||||
<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
|
||||
</router-link>
|
||||
<header>
|
||||
<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
|
||||
<span class="username">@{{ pAcct }}</span>
|
||||
<router-link class="time" :to="`/@${pAcct}/${p.id}`">
|
||||
<mk-time :time="p.createdAt"/>
|
||||
</router-link>
|
||||
</header>
|
||||
<div class="body">
|
||||
<mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.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="p.renote">
|
||||
<mk-note-preview :note="p.renote"/>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer :note="p"/>
|
||||
<button @click="reply" title="返信">
|
||||
%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
</button>
|
||||
<button @click="renote" title="Renote">
|
||||
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||
</button>
|
||||
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション">
|
||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
</button>
|
||||
<button @click="menu" ref="menuButton">
|
||||
%fa:ellipsis-h%
|
||||
</button>
|
||||
</footer>
|
||||
</article>
|
||||
<div class="replies" v-if="!compact">
|
||||
<x-sub v-for="note in replies" :key="note.id" :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import dateStringify from '../../../common/scripts/date-stringify';
|
||||
import getAcct from '../../../../../acct/render';
|
||||
import getUserName from '../../../../../renderers/get-user-name';
|
||||
import parse from '../../../../../text/parse';
|
||||
|
||||
import MkPostFormWindow from './post-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 './note-detail.sub.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XSub
|
||||
},
|
||||
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
compact: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
context: [],
|
||||
contextFetching: false,
|
||||
replies: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds == null &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
p(): any {
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? Object.keys(this.p.reactionCounts)
|
||||
.map(key => this.p.reactionCounts[key])
|
||||
.reduce((a, b) => a + b)
|
||||
: 0;
|
||||
},
|
||||
title(): string {
|
||||
return dateStringify(this.p.createdAt);
|
||||
},
|
||||
acct(): string {
|
||||
return getAcct(this.note.user);
|
||||
},
|
||||
name(): string {
|
||||
return getUserName(this.note.user);
|
||||
},
|
||||
pAcct(): string {
|
||||
return getAcct(this.p.user);
|
||||
},
|
||||
pName(): string {
|
||||
return getUserName(this.p.user);
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Get replies
|
||||
if (!this.compact) {
|
||||
(this as any).api('notes/replies', {
|
||||
noteId: this.p.id,
|
||||
limit: 8
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
});
|
||||
}
|
||||
|
||||
// Draw map
|
||||
if (this.p.geo) {
|
||||
const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
|
||||
if (shouldShowMap) {
|
||||
(this as any).os.getGoogleMaps().then(maps => {
|
||||
const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
|
||||
const map = new maps.Map(this.$refs.map, {
|
||||
center: uluru,
|
||||
zoom: 15
|
||||
});
|
||||
new maps.Marker({
|
||||
position: uluru,
|
||||
map: map
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchContext() {
|
||||
this.contextFetching = true;
|
||||
|
||||
// Fetch context
|
||||
(this as any).api('notes/context', {
|
||||
noteId: this.p.replyId
|
||||
}).then(context => {
|
||||
this.contextFetching = false;
|
||||
this.context = context.reverse();
|
||||
});
|
||||
},
|
||||
reply() {
|
||||
(this as any).os.new(MkPostFormWindow, {
|
||||
reply: this.p
|
||||
});
|
||||
},
|
||||
renote() {
|
||||
(this as any).os.new(MkRenoteFormWindow, {
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
react() {
|
||||
(this as any).os.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
menu() {
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.p
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.mk-note-detail
|
||||
margin 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
text-align left
|
||||
background #fff
|
||||
border solid 1px rgba(0, 0, 0, 0.1)
|
||||
border-radius 8px
|
||||
|
||||
> .read-more
|
||||
display block
|
||||
margin 0
|
||||
padding 10px 0
|
||||
width 100%
|
||||
font-size 1em
|
||||
text-align center
|
||||
color #999
|
||||
cursor pointer
|
||||
background #fafafa
|
||||
outline none
|
||||
border none
|
||||
border-bottom solid 1px #eef0f2
|
||||
border-radius 6px 6px 0 0
|
||||
|
||||
&:hover
|
||||
background #f6f6f6
|
||||
|
||||
&:active
|
||||
background #f0f0f0
|
||||
|
||||
&:disabled
|
||||
color #ccc
|
||||
|
||||
> .context
|
||||
> *
|
||||
border-bottom 1px solid #eef0f2
|
||||
|
||||
> .renote
|
||||
color #9dbb00
|
||||
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 16px 32px
|
||||
|
||||
.avatar-anchor
|
||||
display inline-block
|
||||
|
||||
.avatar
|
||||
vertical-align bottom
|
||||
min-width 28px
|
||||
min-height 28px
|
||||
max-width 28px
|
||||
max-height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
.name
|
||||
font-weight bold
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
|
||||
> .reply-to
|
||||
border-bottom 1px solid #eef0f2
|
||||
|
||||
> article
|
||||
padding 28px 32px 18px 32px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
width 60px
|
||||
height 60px
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 60px
|
||||
height 60px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> header
|
||||
position absolute
|
||||
top 28px
|
||||
left 108px
|
||||
width calc(100% - 108px)
|
||||
|
||||
> .name
|
||||
display inline-block
|
||||
margin 0
|
||||
line-height 24px
|
||||
color #777
|
||||
font-size 18px
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
display block
|
||||
text-align left
|
||||
margin 0
|
||||
color #ccc
|
||||
|
||||
> .time
|
||||
position absolute
|
||||
top 0
|
||||
right 32px
|
||||
font-size 1em
|
||||
color #c0c0c0
|
||||
|
||||
> .body
|
||||
padding 8px 0
|
||||
|
||||
> .renote
|
||||
margin 8px 0
|
||||
|
||||
> .mk-note-preview
|
||||
padding 16px
|
||||
border dashed 1px #c0dac6
|
||||
border-radius 8px
|
||||
|
||||
> .location
|
||||
margin 4px 0
|
||||
font-size 12px
|
||||
color #ccc
|
||||
|
||||
> .map
|
||||
width 100%
|
||||
height 300px
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> .mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .tags
|
||||
margin 4px 0 0 0
|
||||
|
||||
> *
|
||||
display inline-block
|
||||
margin 0 8px 0 0
|
||||
padding 2px 8px 2px 16px
|
||||
font-size 90%
|
||||
color #8d969e
|
||||
background #edf0f3
|
||||
border-radius 4px
|
||||
|
||||
&:before
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
bottom 0
|
||||
left 4px
|
||||
width 8px
|
||||
height 8px
|
||||
margin auto 0
|
||||
background #fff
|
||||
border-radius 100%
|
||||
|
||||
&:hover
|
||||
text-decoration none
|
||||
background #e2e7ec
|
||||
|
||||
> footer
|
||||
font-size 1.2em
|
||||
|
||||
> button
|
||||
margin 0 28px 0 0
|
||||
padding 8px
|
||||
background transparent
|
||||
border none
|
||||
font-size 1em
|
||||
color #ddd
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
color #666
|
||||
|
||||
> .count
|
||||
display inline
|
||||
margin 0 0 0 8px
|
||||
color #999
|
||||
|
||||
&.reacted
|
||||
color $theme-color
|
||||
|
||||
> .replies
|
||||
> *
|
||||
border-top 1px solid #eef0f2
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="stylus" module>
|
||||
.text
|
||||
cursor default
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
overflow-wrap break-word
|
||||
font-size 1.5em
|
||||
color #717171
|
||||
</style>
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="mk-post-preview" :title="title">
|
||||
<div class="mk-note-preview" :title="title">
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`">
|
||||
<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
|
||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
|
||||
</router-link>
|
||||
<div class="main">
|
||||
<header>
|
||||
<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link>
|
||||
<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
|
||||
<span class="username">@{{ acct }}</span>
|
||||
<router-link class="time" :to="`/@${acct}/${post.id}`">
|
||||
<mk-time :time="post.createdAt"/>
|
||||
<router-link class="time" :to="`/@${acct}/${note.id}`">
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</router-link>
|
||||
</header>
|
||||
<div class="body">
|
||||
<mk-sub-post-content class="text" :post="post"/>
|
||||
<mk-sub-note-content class="text" :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,23 +25,23 @@ import getAcct from '../../../../../acct/render';
|
||||
import getUserName from '../../../../../renderers/get-user-name';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
props: ['note'],
|
||||
computed: {
|
||||
acct() {
|
||||
return getAcct(this.post.user);
|
||||
return getAcct(this.note.user);
|
||||
},
|
||||
name() {
|
||||
return getUserName(this.post.user);
|
||||
return getUserName(this.note.user);
|
||||
},
|
||||
title(): string {
|
||||
return dateStringify(this.post.createdAt);
|
||||
return dateStringify(this.note.createdAt);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-post-preview
|
||||
.mk-note-preview
|
||||
font-size 0.9em
|
||||
background #fff
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="sub" :title="title">
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`">
|
||||
<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
|
||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
|
||||
</router-link>
|
||||
<div class="main">
|
||||
<header>
|
||||
<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link>
|
||||
<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
|
||||
<span class="username">@{{ acct }}</span>
|
||||
<router-link class="created-at" :to="`/@${acct}/${post.id}`">
|
||||
<mk-time :time="post.createdAt"/>
|
||||
<router-link class="created-at" :to="`/@${acct}/${note.id}`">
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</router-link>
|
||||
</header>
|
||||
<div class="body">
|
||||
<mk-sub-post-content class="text" :post="post"/>
|
||||
<mk-sub-note-content class="text" :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,16 +25,16 @@ import getAcct from '../../../../../acct/render';
|
||||
import getUserName from '../../../../../renderers/get-user-name';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
props: ['note'],
|
||||
computed: {
|
||||
acct() {
|
||||
return getAcct(this.post.user);
|
||||
return getAcct(this.note.user);
|
||||
},
|
||||
name(): string {
|
||||
return getUserName(this.post.user);
|
||||
return getUserName(this.note.user);
|
||||
},
|
||||
title(): string {
|
||||
return dateStringify(this.post.createdAt);
|
||||
return dateStringify(this.note.createdAt);
|
||||
}
|
||||
}
|
||||
});
|
||||
596
src/client/app/desktop/views/components/notes.note.vue
Normal file
596
src/client/app/desktop/views/components/notes.note.vue
Normal file
@@ -0,0 +1,596 @@
|
||||
<template>
|
||||
<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
|
||||
<div class="reply-to" v-if="p.reply">
|
||||
<x-sub :note="p.reply"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<p>
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
|
||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
|
||||
</router-link>
|
||||
%fa:retweet%
|
||||
<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
|
||||
<a class="name" :href="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
|
||||
<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
|
||||
</p>
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</div>
|
||||
<article>
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`">
|
||||
<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
|
||||
</router-link>
|
||||
<div class="main">
|
||||
<header>
|
||||
<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link>
|
||||
<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
|
||||
<span class="username">@{{ acct }}</span>
|
||||
<div class="info">
|
||||
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
|
||||
<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
|
||||
<router-link class="created-at" :to="url">
|
||||
<mk-time :time="p.createdAt"/>
|
||||
</router-link>
|
||||
</div>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p class="channel" v-if="p.channel">
|
||||
<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
|
||||
</p>
|
||||
<div class="text">
|
||||
<a class="reply" v-if="p.reply">%fa:reply%</a>
|
||||
<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="p.renote">RP:</a>
|
||||
</div>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.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="p.renote">
|
||||
<mk-note-preview :note="p.renote"/>
|
||||
</div>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
||||
<button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%">
|
||||
%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
</button>
|
||||
<button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%">
|
||||
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||
</button>
|
||||
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.add-reaction%">
|
||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
</button>
|
||||
<button @click="menu" ref="menuButton">
|
||||
%fa:ellipsis-h%
|
||||
</button>
|
||||
<button title="%i18n:desktop.tags.mk-timeline-note.detail">
|
||||
<template v-if="!isDetailOpened">%fa:caret-down%</template>
|
||||
<template v-if="isDetailOpened">%fa:caret-up%</template>
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
<div class="detail" v-if="isDetailOpened">
|
||||
<mk-note-status-graph width="462" height="130" :note="p"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import dateStringify from '../../../common/scripts/date-stringify';
|
||||
import getAcct from '../../../../../acct/render';
|
||||
import getUserName from '../../../../../renderers/get-user-name';
|
||||
import parse from '../../../../../text/parse';
|
||||
|
||||
import MkPostFormWindow from './post-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';
|
||||
|
||||
function focus(el, fn) {
|
||||
const target = fn(el);
|
||||
if (target) {
|
||||
if (target.hasAttribute('tabindex')) {
|
||||
target.focus();
|
||||
} else {
|
||||
focus(target, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XSub
|
||||
},
|
||||
|
||||
props: ['note'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isDetailOpened: false,
|
||||
connection: null,
|
||||
connectionId: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
acct(): string {
|
||||
return getAcct(this.p.user);
|
||||
},
|
||||
name(): string {
|
||||
return getUserName(this.p.user);
|
||||
},
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds == null &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
p(): any {
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? Object.keys(this.p.reactionCounts)
|
||||
.map(key => this.p.reactionCounts[key])
|
||||
.reduce((a, b) => a + b)
|
||||
: 0;
|
||||
},
|
||||
title(): string {
|
||||
return dateStringify(this.p.createdAt);
|
||||
},
|
||||
url(): string {
|
||||
return `/@${this.acct}/${this.p.id}`;
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if ((this as any).os.isSignedIn) {
|
||||
this.connection = (this as any).os.stream.getConnection();
|
||||
this.connectionId = (this as any).os.stream.use();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.capture(true);
|
||||
|
||||
if ((this as any).os.isSignedIn) {
|
||||
this.connection.on('_connected_', this.onStreamConnected);
|
||||
}
|
||||
|
||||
// Draw map
|
||||
if (this.p.geo) {
|
||||
const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
|
||||
if (shouldShowMap) {
|
||||
(this as any).os.getGoogleMaps().then(maps => {
|
||||
const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
|
||||
const map = new maps.Map(this.$refs.map, {
|
||||
center: uluru,
|
||||
zoom: 15
|
||||
});
|
||||
new maps.Marker({
|
||||
position: uluru,
|
||||
map: map
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.decapture(true);
|
||||
|
||||
if ((this as any).os.isSignedIn) {
|
||||
this.connection.off('_connected_', this.onStreamConnected);
|
||||
(this as any).os.stream.dispose(this.connectionId);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
capture(withHandler = false) {
|
||||
if ((this as any).os.isSignedIn) {
|
||||
this.connection.send({
|
||||
type: 'capture',
|
||||
id: this.p.id
|
||||
});
|
||||
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
|
||||
}
|
||||
},
|
||||
decapture(withHandler = false) {
|
||||
if ((this as any).os.isSignedIn) {
|
||||
this.connection.send({
|
||||
type: 'decapture',
|
||||
id: this.p.id
|
||||
});
|
||||
if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
|
||||
}
|
||||
},
|
||||
onStreamConnected() {
|
||||
this.capture();
|
||||
},
|
||||
onStreamNoteUpdated(data) {
|
||||
const note = data.note;
|
||||
if (note.id == this.note.id) {
|
||||
this.$emit('update:note', note);
|
||||
} else if (note.id == this.note.renoteId) {
|
||||
this.note.renote = note;
|
||||
}
|
||||
},
|
||||
reply() {
|
||||
(this as any).os.new(MkPostFormWindow, {
|
||||
reply: this.p
|
||||
});
|
||||
},
|
||||
renote() {
|
||||
(this as any).os.new(MkRenoteFormWindow, {
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
react() {
|
||||
(this as any).os.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
menu() {
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
onKeydown(e) {
|
||||
let shouldBeCancel = true;
|
||||
|
||||
switch (true) {
|
||||
case e.which == 38: // [↑]
|
||||
case e.which == 74: // [j]
|
||||
case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
|
||||
focus(this.$el, e => e.previousElementSibling);
|
||||
break;
|
||||
|
||||
case e.which == 40: // [↓]
|
||||
case e.which == 75: // [k]
|
||||
case e.which == 9: // [Tab]
|
||||
focus(this.$el, e => e.nextElementSibling);
|
||||
break;
|
||||
|
||||
case e.which == 81: // [q]
|
||||
case e.which == 69: // [e]
|
||||
this.renote();
|
||||
break;
|
||||
|
||||
case e.which == 70: // [f]
|
||||
case e.which == 76: // [l]
|
||||
//this.like();
|
||||
break;
|
||||
|
||||
case e.which == 82: // [r]
|
||||
this.reply();
|
||||
break;
|
||||
|
||||
default:
|
||||
shouldBeCancel = false;
|
||||
}
|
||||
|
||||
if (shouldBeCancel) e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.note
|
||||
margin 0
|
||||
padding 0
|
||||
background #fff
|
||||
border-bottom solid 1px #eaeaea
|
||||
|
||||
&:first-child
|
||||
border-top-left-radius 6px
|
||||
border-top-right-radius 6px
|
||||
|
||||
> .renote
|
||||
border-top-left-radius 6px
|
||||
border-top-right-radius 6px
|
||||
|
||||
&:last-of-type
|
||||
border-bottom none
|
||||
|
||||
&:focus
|
||||
z-index 1
|
||||
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top 2px
|
||||
right 2px
|
||||
bottom 2px
|
||||
left 2px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 4px
|
||||
|
||||
> .renote
|
||||
color #9dbb00
|
||||
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 16px 32px
|
||||
line-height 28px
|
||||
|
||||
.avatar-anchor
|
||||
display inline-block
|
||||
|
||||
.avatar
|
||||
vertical-align bottom
|
||||
width 28px
|
||||
height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
.name
|
||||
font-weight bold
|
||||
|
||||
> .mk-time
|
||||
position absolute
|
||||
top 16px
|
||||
right 32px
|
||||
font-size 0.9em
|
||||
line-height 28px
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
|
||||
> .reply-to
|
||||
padding 0 16px
|
||||
background rgba(0, 0, 0, 0.0125)
|
||||
|
||||
> .mk-note-preview
|
||||
background transparent
|
||||
|
||||
> article
|
||||
padding 28px 32px 18px 32px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 16px 10px 0
|
||||
//position -webkit-sticky
|
||||
//position sticky
|
||||
//top 74px
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 58px
|
||||
height 58px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 74px)
|
||||
|
||||
> header
|
||||
display flex
|
||||
align-items center
|
||||
margin-bottom 4px
|
||||
white-space nowrap
|
||||
|
||||
> .name
|
||||
display block
|
||||
margin 0 .5em 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
color #627079
|
||||
font-size 1em
|
||||
font-weight bold
|
||||
text-decoration none
|
||||
text-overflow ellipsis
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .is-bot
|
||||
margin 0 .5em 0 0
|
||||
padding 1px 6px
|
||||
font-size 12px
|
||||
color #aaa
|
||||
border solid 1px #ddd
|
||||
border-radius 3px
|
||||
|
||||
> .username
|
||||
margin 0 .5em 0 0
|
||||
color #ccc
|
||||
|
||||
> .info
|
||||
margin-left auto
|
||||
font-size 0.9em
|
||||
|
||||
> .mobile
|
||||
margin-right 8px
|
||||
color #ccc
|
||||
|
||||
> .app
|
||||
margin-right 8px
|
||||
padding-right 8px
|
||||
color #ccc
|
||||
border-right solid 1px #eaeaea
|
||||
|
||||
> .created-at
|
||||
color #c0c0c0
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
overflow-wrap break-word
|
||||
font-size 1.1em
|
||||
color #717171
|
||||
|
||||
>>> .quote
|
||||
margin 8px
|
||||
padding 6px 12px
|
||||
color #aaa
|
||||
border-left solid 3px #eee
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color #717171
|
||||
|
||||
> .rp
|
||||
margin-left 4px
|
||||
font-style oblique
|
||||
color #a0bf46
|
||||
|
||||
> .location
|
||||
margin 4px 0
|
||||
font-size 12px
|
||||
color #ccc
|
||||
|
||||
> .map
|
||||
width 100%
|
||||
height 300px
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> .tags
|
||||
margin 4px 0 0 0
|
||||
|
||||
> *
|
||||
display inline-block
|
||||
margin 0 8px 0 0
|
||||
padding 2px 8px 2px 16px
|
||||
font-size 90%
|
||||
color #8d969e
|
||||
background #edf0f3
|
||||
border-radius 4px
|
||||
|
||||
&:before
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
bottom 0
|
||||
left 4px
|
||||
width 8px
|
||||
height 8px
|
||||
margin auto 0
|
||||
background #fff
|
||||
border-radius 100%
|
||||
|
||||
&:hover
|
||||
text-decoration none
|
||||
background #e2e7ec
|
||||
|
||||
.mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .channel
|
||||
margin 0
|
||||
|
||||
> .mk-poll
|
||||
font-size 80%
|
||||
|
||||
> .renote
|
||||
margin 8px 0
|
||||
|
||||
> .mk-note-preview
|
||||
padding 16px
|
||||
border dashed 1px #c0dac6
|
||||
border-radius 8px
|
||||
|
||||
> footer
|
||||
> button
|
||||
margin 0 28px 0 0
|
||||
padding 0 8px
|
||||
line-height 32px
|
||||
font-size 1em
|
||||
color #ddd
|
||||
background transparent
|
||||
border none
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
color #666
|
||||
|
||||
> .count
|
||||
display inline
|
||||
margin 0 0 0 8px
|
||||
color #999
|
||||
|
||||
&.reacted
|
||||
color $theme-color
|
||||
|
||||
&:last-child
|
||||
position absolute
|
||||
right 0
|
||||
margin 0
|
||||
|
||||
> .detail
|
||||
padding-top 4px
|
||||
background rgba(0, 0, 0, 0.0125)
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="stylus" module>
|
||||
.text
|
||||
|
||||
code
|
||||
padding 4px 8px
|
||||
margin 0 0.5em
|
||||
font-size 80%
|
||||
color #525252
|
||||
background #f8f8f8
|
||||
border-radius 2px
|
||||
|
||||
pre > code
|
||||
padding 16px
|
||||
margin 0
|
||||
|
||||
[data-is-me]:after
|
||||
content "you"
|
||||
padding 0 4px
|
||||
margin-left 4px
|
||||
font-size 80%
|
||||
color $theme-color-foreground
|
||||
background $theme-color
|
||||
border-radius 4px
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="mk-posts">
|
||||
<template v-for="(post, i) in _posts">
|
||||
<x-post :post="post" :key="post.id" @update:post="onPostUpdated(i, $event)"/>
|
||||
<p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
|
||||
<span>%fa:angle-up%{{ post._datetext }}</span>
|
||||
<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
|
||||
<div class="mk-notes">
|
||||
<template v-for="(note, i) in _notes">
|
||||
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
||||
<p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||
<span>%fa:angle-up%{{ note._datetext }}</span>
|
||||
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
<footer>
|
||||
@@ -15,26 +15,26 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XPost from './posts.post.vue';
|
||||
import XNote from './notes.note.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPost
|
||||
XNote
|
||||
},
|
||||
props: {
|
||||
posts: {
|
||||
notes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_posts(): any[] {
|
||||
return (this.posts as any).map(post => {
|
||||
const date = new Date(post.createdAt).getDate();
|
||||
const month = new Date(post.createdAt).getMonth() + 1;
|
||||
post._date = date;
|
||||
post._datetext = `${month}月 ${date}日`;
|
||||
return post;
|
||||
_notes(): any[] {
|
||||
return (this.notes as any).map(note => {
|
||||
const date = new Date(note.createdAt).getDate();
|
||||
const month = new Date(note.createdAt).getMonth() + 1;
|
||||
note._date = date;
|
||||
note._datetext = `${month}月 ${date}日`;
|
||||
return note;
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -42,15 +42,15 @@ export default Vue.extend({
|
||||
focus() {
|
||||
(this.$el as any).children[0].focus();
|
||||
},
|
||||
onPostUpdated(i, post) {
|
||||
Vue.set((this as any).posts, i, post);
|
||||
onNoteUpdated(i, note) {
|
||||
Vue.set((this as any).notes, i, note);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-posts
|
||||
.mk-notes
|
||||
|
||||
> .date
|
||||
display block
|
||||
@@ -13,33 +13,33 @@
|
||||
<mk-reaction-icon :reaction="notification.reaction"/>
|
||||
<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
|
||||
</p>
|
||||
<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
|
||||
%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
|
||||
<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
|
||||
%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="notification.type == 'repost'">
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
|
||||
<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
|
||||
<template v-if="notification.type == 'renote'">
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
|
||||
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
|
||||
</router-link>
|
||||
<div class="text">
|
||||
<p>%fa:retweet%
|
||||
<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
|
||||
<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
|
||||
</p>
|
||||
<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
|
||||
%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
|
||||
<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
|
||||
%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="notification.type == 'quote'">
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
|
||||
<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
|
||||
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
|
||||
</router-link>
|
||||
<div class="text">
|
||||
<p>%fa:quote-left%
|
||||
<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
|
||||
<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
|
||||
</p>
|
||||
<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
|
||||
<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="notification.type == 'follow'">
|
||||
@@ -53,25 +53,25 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="notification.type == 'reply'">
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
|
||||
<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
|
||||
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
|
||||
</router-link>
|
||||
<div class="text">
|
||||
<p>%fa:reply%
|
||||
<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
|
||||
<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
|
||||
</p>
|
||||
<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
|
||||
<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="notification.type == 'mention'">
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
|
||||
<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
|
||||
<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
|
||||
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
|
||||
</router-link>
|
||||
<div class="text">
|
||||
<p>%fa:at%
|
||||
<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
|
||||
<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
|
||||
</p>
|
||||
<a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
|
||||
<a class="note-preview" :href="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="notification.type == 'poll_vote'">
|
||||
@@ -80,8 +80,8 @@
|
||||
</router-link>
|
||||
<div class="text">
|
||||
<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</a></p>
|
||||
<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
|
||||
%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
|
||||
<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
|
||||
%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
@@ -103,7 +103,7 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import getAcct from '../../../../../acct/render';
|
||||
import getPostSummary from '../../../../../renderers/get-post-summary';
|
||||
import getNoteSummary from '../../../../../renderers/get-note-summary';
|
||||
import getUserName from '../../../../../renderers/get-user-name';
|
||||
|
||||
export default Vue.extend({
|
||||
@@ -115,7 +115,7 @@ export default Vue.extend({
|
||||
moreNotifications: false,
|
||||
connection: null,
|
||||
connectionId: null,
|
||||
getPostSummary
|
||||
getNoteSummary
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -241,10 +241,10 @@ export default Vue.extend({
|
||||
i, .mk-reaction-icon
|
||||
margin-right 4px
|
||||
|
||||
.post-preview
|
||||
.note-preview
|
||||
color rgba(0, 0, 0, 0.7)
|
||||
|
||||
.post-ref
|
||||
.note-ref
|
||||
color rgba(0, 0, 0, 0.7)
|
||||
|
||||
[data-fa]
|
||||
@@ -254,7 +254,7 @@ export default Vue.extend({
|
||||
display inline-block
|
||||
margin-right 3px
|
||||
|
||||
&.repost, &.quote
|
||||
&.renote, &.quote
|
||||
.text p i
|
||||
color #77B255
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mk-post-detail" :title="title">
|
||||
<div class="mk-note-detail" :title="title">
|
||||
<button
|
||||
class="read-more"
|
||||
v-if="p.reply && p.reply.replyId && context == null"
|
||||
@@ -11,19 +11,19 @@
|
||||
<template v-if="contextFetching">%fa:spinner .pulse%</template>
|
||||
</button>
|
||||
<div class="context">
|
||||
<x-sub v-for="post in context" :key="post.id" :post="post"/>
|
||||
<x-sub v-for="note in context" :key="note.id" :note="note"/>
|
||||
</div>
|
||||
<div class="reply-to" v-if="p.reply">
|
||||
<x-sub :post="p.reply"/>
|
||||
<x-sub :note="p.reply"/>
|
||||
</div>
|
||||
<div class="repost" v-if="isRepost">
|
||||
<div class="renote" v-if="isRenote">
|
||||
<p>
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
|
||||
<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
|
||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
|
||||
</router-link>
|
||||
%fa:retweet%
|
||||
<router-link class="name" :href="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
|
||||
がRepost
|
||||
<router-link class="name" :href="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
|
||||
がRenote
|
||||
</p>
|
||||
</div>
|
||||
<article>
|
||||
@@ -38,28 +38,28 @@
|
||||
</router-link>
|
||||
</header>
|
||||
<div class="body">
|
||||
<mk-post-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
|
||||
<mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :post="p"/>
|
||||
<mk-poll v-if="p.poll" :note="p"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
|
||||
<div class="map" v-if="p.geo" ref="map"></div>
|
||||
<div class="repost" v-if="p.repost">
|
||||
<mk-post-preview :post="p.repost"/>
|
||||
<div class="renote" v-if="p.renote">
|
||||
<mk-note-preview :note="p.renote"/>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer :post="p"/>
|
||||
<mk-reactions-viewer :note="p"/>
|
||||
<button @click="reply" title="返信">
|
||||
%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
</button>
|
||||
<button @click="repost" title="Repost">
|
||||
%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p>
|
||||
<button @click="renote" title="Renote">
|
||||
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||
</button>
|
||||
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション">
|
||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
@@ -70,7 +70,7 @@
|
||||
</footer>
|
||||
</article>
|
||||
<div class="replies" v-if="!compact">
|
||||
<x-sub v-for="post in replies" :key="post.id" :post="post"/>
|
||||
<x-sub v-for="note in replies" :key="note.id" :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -83,10 +83,10 @@ import getUserName from '../../../../../renderers/get-user-name';
|
||||
import parse from '../../../../../text/parse';
|
||||
|
||||
import MkPostFormWindow from './post-form-window.vue';
|
||||
import MkRepostFormWindow from './repost-form-window.vue';
|
||||
import MkPostMenu from '../../../common/views/components/post-menu.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 './post-detail.sub.vue';
|
||||
import XSub from './note-detail.sub.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@@ -94,7 +94,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
props: {
|
||||
post: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
@@ -112,14 +112,14 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
computed: {
|
||||
isRepost(): boolean {
|
||||
return (this.post.repost &&
|
||||
this.post.text == null &&
|
||||
this.post.mediaIds == null &&
|
||||
this.post.poll == null);
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds == null &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
p(): any {
|
||||
return this.isRepost ? this.post.repost : this.post;
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
@@ -132,10 +132,10 @@ export default Vue.extend({
|
||||
return dateStringify(this.p.createdAt);
|
||||
},
|
||||
acct(): string {
|
||||
return getAcct(this.post.user);
|
||||
return getAcct(this.note.user);
|
||||
},
|
||||
name(): string {
|
||||
return getUserName(this.post.user);
|
||||
return getUserName(this.note.user);
|
||||
},
|
||||
pAcct(): string {
|
||||
return getAcct(this.p.user);
|
||||
@@ -158,8 +158,8 @@ export default Vue.extend({
|
||||
mounted() {
|
||||
// Get replies
|
||||
if (!this.compact) {
|
||||
(this as any).api('posts/replies', {
|
||||
postId: this.p.id,
|
||||
(this as any).api('notes/replies', {
|
||||
noteId: this.p.id,
|
||||
limit: 8
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
@@ -190,8 +190,8 @@ export default Vue.extend({
|
||||
this.contextFetching = true;
|
||||
|
||||
// Fetch context
|
||||
(this as any).api('posts/context', {
|
||||
postId: this.p.replyId
|
||||
(this as any).api('notes/context', {
|
||||
noteId: this.p.replyId
|
||||
}).then(context => {
|
||||
this.contextFetching = false;
|
||||
this.context = context.reverse();
|
||||
@@ -202,21 +202,21 @@ export default Vue.extend({
|
||||
reply: this.p
|
||||
});
|
||||
},
|
||||
repost() {
|
||||
(this as any).os.new(MkRepostFormWindow, {
|
||||
post: this.p
|
||||
renote() {
|
||||
(this as any).os.new(MkRenoteFormWindow, {
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
react() {
|
||||
(this as any).os.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
post: this.p
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
menu() {
|
||||
(this as any).os.new(MkPostMenu, {
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
post: this.p
|
||||
note: this.p
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -226,7 +226,7 @@ export default Vue.extend({
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.mk-post-detail
|
||||
.mk-note-detail
|
||||
margin 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
@@ -263,7 +263,7 @@ export default Vue.extend({
|
||||
> *
|
||||
border-bottom 1px solid #eef0f2
|
||||
|
||||
> .repost
|
||||
> .renote
|
||||
color #9dbb00
|
||||
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||
|
||||
@@ -355,10 +355,10 @@ export default Vue.extend({
|
||||
> .body
|
||||
padding 8px 0
|
||||
|
||||
> .repost
|
||||
> .renote
|
||||
margin 8px 0
|
||||
|
||||
> .mk-post-preview
|
||||
> .mk-note-preview
|
||||
padding 16px
|
||||
border dashed 1px #c0dac6
|
||||
border-radius 8px
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<mk-window ref="window" is-modal @closed="$destroy">
|
||||
<span slot="header">
|
||||
<span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</span>
|
||||
<span v-if="!reply">%i18n:desktop.tags.mk-post-form-window.post%</span>
|
||||
<span v-if="!reply">%i18n:desktop.tags.mk-post-form-window.note%</span>
|
||||
<span v-if="reply">%i18n:desktop.tags.mk-post-form-window.reply%</span>
|
||||
<span :class="$style.count" v-if="media.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', media.length) }}</span>
|
||||
<span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
|
||||
</span>
|
||||
|
||||
<mk-post-preview v-if="reply" :class="$style.postPreview" :post="reply"/>
|
||||
<mk-note-preview v-if="reply" :class="$style.notePreview" :note="reply"/>
|
||||
<mk-post-form ref="form"
|
||||
:reply="reply"
|
||||
@posted="onPosted"
|
||||
@@ -70,7 +70,7 @@ export default Vue.extend({
|
||||
&:after
|
||||
content ')'
|
||||
|
||||
.postPreview
|
||||
.notePreview
|
||||
margin 16px 22px
|
||||
|
||||
</style>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default Vue.extend({
|
||||
components: {
|
||||
XDraggable
|
||||
},
|
||||
props: ['reply', 'repost'],
|
||||
props: ['reply', 'renote'],
|
||||
data() {
|
||||
return {
|
||||
posting: false,
|
||||
@@ -61,28 +61,28 @@ export default Vue.extend({
|
||||
},
|
||||
computed: {
|
||||
draftId(): string {
|
||||
return this.repost
|
||||
? 'repost:' + this.repost.id
|
||||
return this.renote
|
||||
? 'renote:' + this.renote.id
|
||||
: this.reply
|
||||
? 'reply:' + this.reply.id
|
||||
: 'post';
|
||||
: 'note';
|
||||
},
|
||||
placeholder(): string {
|
||||
return this.repost
|
||||
return this.renote
|
||||
? '%i18n:desktop.tags.mk-post-form.quote-placeholder%'
|
||||
: this.reply
|
||||
? '%i18n:desktop.tags.mk-post-form.reply-placeholder%'
|
||||
: '%i18n:desktop.tags.mk-post-form.post-placeholder%';
|
||||
: '%i18n:desktop.tags.mk-post-form.note-placeholder%';
|
||||
},
|
||||
submitText(): string {
|
||||
return this.repost
|
||||
? '%i18n:desktop.tags.mk-post-form.repost%'
|
||||
return this.renote
|
||||
? '%i18n:desktop.tags.mk-post-form.renote%'
|
||||
: this.reply
|
||||
? '%i18n:desktop.tags.mk-post-form.reply%'
|
||||
: '%i18n:desktop.tags.mk-post-form.post%';
|
||||
: '%i18n:desktop.tags.mk-post-form.note%';
|
||||
},
|
||||
canPost(): boolean {
|
||||
return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.repost);
|
||||
return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -217,11 +217,11 @@ export default Vue.extend({
|
||||
post() {
|
||||
this.posting = true;
|
||||
|
||||
(this as any).api('posts/create', {
|
||||
(this as any).api('notes/create', {
|
||||
text: this.text == '' ? undefined : this.text,
|
||||
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
|
||||
replyId: this.reply ? this.reply.id : undefined,
|
||||
repostId: this.repost ? this.repost.id : undefined,
|
||||
renoteId: this.renote ? this.renote.id : undefined,
|
||||
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
|
||||
geo: this.geo ? {
|
||||
coordinates: [this.geo.longitude, this.geo.latitude],
|
||||
@@ -235,17 +235,17 @@ export default Vue.extend({
|
||||
this.clear();
|
||||
this.deleteDraft();
|
||||
this.$emit('posted');
|
||||
(this as any).apis.notify(this.repost
|
||||
(this as any).apis.notify(this.renote
|
||||
? '%i18n:desktop.tags.mk-post-form.reposted%'
|
||||
: this.reply
|
||||
? '%i18n:desktop.tags.mk-post-form.replied%'
|
||||
: '%i18n:desktop.tags.mk-post-form.posted%');
|
||||
}).catch(err => {
|
||||
(this as any).apis.notify(this.repost
|
||||
? '%i18n:desktop.tags.mk-post-form.repost-failed%'
|
||||
(this as any).apis.notify(this.renote
|
||||
? '%i18n:desktop.tags.mk-post-form.renote-failed%'
|
||||
: this.reply
|
||||
? '%i18n:desktop.tags.mk-post-form.reply-failed%'
|
||||
: '%i18n:desktop.tags.mk-post-form.post-failed%');
|
||||
: '%i18n:desktop.tags.mk-post-form.note-failed%');
|
||||
}).then(() => {
|
||||
this.posting = false;
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="post" tabindex="-1" :title="title" @keydown="onKeydown">
|
||||
<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
|
||||
<div class="reply-to" v-if="p.reply">
|
||||
<x-sub :post="p.reply"/>
|
||||
<x-sub :note="p.reply"/>
|
||||
</div>
|
||||
<div class="repost" v-if="isRepost">
|
||||
<div class="renote" v-if="isRenote">
|
||||
<p>
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
|
||||
<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
|
||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
|
||||
</router-link>
|
||||
%fa:retweet%
|
||||
<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
|
||||
<a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</a>
|
||||
<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
|
||||
<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
|
||||
<a class="name" :href="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
|
||||
<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
|
||||
</p>
|
||||
<mk-time :time="post.createdAt"/>
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</div>
|
||||
<article>
|
||||
<router-link class="avatar-anchor" :to="`/@${acct}`">
|
||||
@@ -38,38 +38,38 @@
|
||||
</p>
|
||||
<div class="text">
|
||||
<a class="reply" v-if="p.reply">%fa:reply%</a>
|
||||
<mk-post-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="p.repost">RP:</a>
|
||||
<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
|
||||
<a class="rp" v-if="p.renote">RP:</a>
|
||||
</div>
|
||||
<div class="media" v-if="p.media.length > 0">
|
||||
<mk-media-list :media-list="p.media"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
|
||||
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
|
||||
<div class="tags" v-if="p.tags && p.tags.length > 0">
|
||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||
</div>
|
||||
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
|
||||
<div class="map" v-if="p.geo" ref="map"></div>
|
||||
<div class="repost" v-if="p.repost">
|
||||
<mk-post-preview :post="p.repost"/>
|
||||
<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"/>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer :post="p" ref="reactionsViewer"/>
|
||||
<button @click="reply" title="%i18n:desktop.tags.mk-timeline-post.reply%">
|
||||
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
|
||||
<button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%">
|
||||
%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
</button>
|
||||
<button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%">
|
||||
%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p>
|
||||
<button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%">
|
||||
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||
</button>
|
||||
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
|
||||
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.add-reaction%">
|
||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
</button>
|
||||
<button @click="menu" ref="menuButton">
|
||||
%fa:ellipsis-h%
|
||||
</button>
|
||||
<button title="%i18n:desktop.tags.mk-timeline-post.detail">
|
||||
<button title="%i18n:desktop.tags.mk-timeline-note.detail">
|
||||
<template v-if="!isDetailOpened">%fa:caret-down%</template>
|
||||
<template v-if="isDetailOpened">%fa:caret-up%</template>
|
||||
</button>
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
</article>
|
||||
<div class="detail" v-if="isDetailOpened">
|
||||
<mk-post-status-graph width="462" height="130" :post="p"/>
|
||||
<mk-note-status-graph width="462" height="130" :note="p"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -90,10 +90,10 @@ import getUserName from '../../../../../renderers/get-user-name';
|
||||
import parse from '../../../../../text/parse';
|
||||
|
||||
import MkPostFormWindow from './post-form-window.vue';
|
||||
import MkRepostFormWindow from './repost-form-window.vue';
|
||||
import MkPostMenu from '../../../common/views/components/post-menu.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 './posts.post.sub.vue';
|
||||
import XSub from './notes.note.sub.vue';
|
||||
|
||||
function focus(el, fn) {
|
||||
const target = fn(el);
|
||||
@@ -111,7 +111,7 @@ export default Vue.extend({
|
||||
XSub
|
||||
},
|
||||
|
||||
props: ['post'],
|
||||
props: ['note'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
@@ -128,14 +128,14 @@ export default Vue.extend({
|
||||
name(): string {
|
||||
return getUserName(this.p.user);
|
||||
},
|
||||
isRepost(): boolean {
|
||||
return (this.post.repost &&
|
||||
this.post.text == null &&
|
||||
this.post.mediaIds == null &&
|
||||
this.post.poll == null);
|
||||
isRenote(): boolean {
|
||||
return (this.note.renote &&
|
||||
this.note.text == null &&
|
||||
this.note.mediaIds == null &&
|
||||
this.note.poll == null);
|
||||
},
|
||||
p(): any {
|
||||
return this.isRepost ? this.post.repost : this.post;
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
@@ -211,7 +211,7 @@ export default Vue.extend({
|
||||
type: 'capture',
|
||||
id: this.p.id
|
||||
});
|
||||
if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
|
||||
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
|
||||
}
|
||||
},
|
||||
decapture(withHandler = false) {
|
||||
@@ -220,18 +220,18 @@ export default Vue.extend({
|
||||
type: 'decapture',
|
||||
id: this.p.id
|
||||
});
|
||||
if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
|
||||
if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
|
||||
}
|
||||
},
|
||||
onStreamConnected() {
|
||||
this.capture();
|
||||
},
|
||||
onStreamPostUpdated(data) {
|
||||
const post = data.post;
|
||||
if (post.id == this.post.id) {
|
||||
this.$emit('update:post', post);
|
||||
} else if (post.id == this.post.repostId) {
|
||||
this.post.repost = post;
|
||||
onStreamNoteUpdated(data) {
|
||||
const note = data.note;
|
||||
if (note.id == this.note.id) {
|
||||
this.$emit('update:note', note);
|
||||
} else if (note.id == this.note.renoteId) {
|
||||
this.note.renote = note;
|
||||
}
|
||||
},
|
||||
reply() {
|
||||
@@ -239,21 +239,21 @@ export default Vue.extend({
|
||||
reply: this.p
|
||||
});
|
||||
},
|
||||
repost() {
|
||||
(this as any).os.new(MkRepostFormWindow, {
|
||||
post: this.p
|
||||
renote() {
|
||||
(this as any).os.new(MkRenoteFormWindow, {
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
react() {
|
||||
(this as any).os.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
post: this.p
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
menu() {
|
||||
(this as any).os.new(MkPostMenu, {
|
||||
(this as any).os.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
post: this.p
|
||||
note: this.p
|
||||
});
|
||||
},
|
||||
onKeydown(e) {
|
||||
@@ -274,7 +274,7 @@ export default Vue.extend({
|
||||
|
||||
case e.which == 81: // [q]
|
||||
case e.which == 69: // [e]
|
||||
this.repost();
|
||||
this.renote();
|
||||
break;
|
||||
|
||||
case e.which == 70: // [f]
|
||||
@@ -299,7 +299,7 @@ export default Vue.extend({
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.post
|
||||
.note
|
||||
margin 0
|
||||
padding 0
|
||||
background #fff
|
||||
@@ -309,7 +309,7 @@ export default Vue.extend({
|
||||
border-top-left-radius 6px
|
||||
border-top-right-radius 6px
|
||||
|
||||
> .repost
|
||||
> .renote
|
||||
border-top-left-radius 6px
|
||||
border-top-right-radius 6px
|
||||
|
||||
@@ -330,7 +330,7 @@ export default Vue.extend({
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 4px
|
||||
|
||||
> .repost
|
||||
> .renote
|
||||
color #9dbb00
|
||||
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||
|
||||
@@ -369,7 +369,7 @@ export default Vue.extend({
|
||||
padding 0 16px
|
||||
background rgba(0, 0, 0, 0.0125)
|
||||
|
||||
> .mk-post-preview
|
||||
> .mk-note-preview
|
||||
background transparent
|
||||
|
||||
> article
|
||||
@@ -529,10 +529,10 @@ export default Vue.extend({
|
||||
> .mk-poll
|
||||
font-size 80%
|
||||
|
||||
> .repost
|
||||
> .renote
|
||||
margin 8px 0
|
||||
|
||||
> .mk-post-preview
|
||||
> .mk-note-preview
|
||||
padding 16px
|
||||
border dashed 1px #c0dac6
|
||||
border-radius 8px
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal @closed="$destroy">
|
||||
<span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-renote-form-window.title%</span>
|
||||
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
|
||||
</mk-window>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['note'],
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onDocumentKeydown);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onDocumentKeydown);
|
||||
},
|
||||
methods: {
|
||||
onDocumentKeydown(e) {
|
||||
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
|
||||
if (e.which == 27) { // Esc
|
||||
(this.$refs.window as any).close();
|
||||
}
|
||||
}
|
||||
},
|
||||
onPosted() {
|
||||
(this.$refs.window as any).close();
|
||||
},
|
||||
onCanceled() {
|
||||
(this.$refs.window as any).close();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" module>
|
||||
.header
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
</style>
|
||||
131
src/client/app/desktop/views/components/renote-form.vue
Normal file
131
src/client/app/desktop/views/components/renote-form.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="mk-renote-form">
|
||||
<mk-note-preview :note="note"/>
|
||||
<template v-if="!quote">
|
||||
<footer>
|
||||
<a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-renote-form.quote%</a>
|
||||
<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-renote-form.cancel%</button>
|
||||
<button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-renote-form.reposting%' : '%i18n:desktop.tags.mk-renote-form.renote%' }}</button>
|
||||
</footer>
|
||||
</template>
|
||||
<template v-if="quote">
|
||||
<mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['note'],
|
||||
data() {
|
||||
return {
|
||||
wait: false,
|
||||
quote: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
ok() {
|
||||
this.wait = true;
|
||||
(this as any).api('notes/create', {
|
||||
renoteId: this.note.id
|
||||
}).then(data => {
|
||||
this.$emit('posted');
|
||||
(this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.success%');
|
||||
}).catch(err => {
|
||||
(this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.failure%');
|
||||
}).then(() => {
|
||||
this.wait = false;
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('canceled');
|
||||
},
|
||||
onQuote() {
|
||||
this.quote = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.form as any).focus();
|
||||
});
|
||||
},
|
||||
onChildFormPosted() {
|
||||
this.$emit('posted');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.mk-renote-form
|
||||
|
||||
> .mk-note-preview
|
||||
margin 16px 22px
|
||||
|
||||
> footer
|
||||
height 72px
|
||||
background lighten($theme-color, 95%)
|
||||
|
||||
> .quote
|
||||
position absolute
|
||||
bottom 16px
|
||||
left 28px
|
||||
line-height 40px
|
||||
|
||||
button
|
||||
display block
|
||||
position absolute
|
||||
bottom 16px
|
||||
cursor pointer
|
||||
padding 0
|
||||
margin 0
|
||||
width 120px
|
||||
height 40px
|
||||
font-size 1em
|
||||
outline none
|
||||
border-radius 4px
|
||||
|
||||
&:focus
|
||||
&:after
|
||||
content ""
|
||||
pointer-events none
|
||||
position absolute
|
||||
top -5px
|
||||
right -5px
|
||||
bottom -5px
|
||||
left -5px
|
||||
border 2px solid rgba($theme-color, 0.3)
|
||||
border-radius 8px
|
||||
|
||||
> .cancel
|
||||
right 148px
|
||||
color #888
|
||||
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
||||
border solid 1px #e2e2e2
|
||||
|
||||
&:hover
|
||||
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
|
||||
border-color #dcdcdc
|
||||
|
||||
&:active
|
||||
background #ececec
|
||||
border-color #dcdcdc
|
||||
|
||||
> .ok
|
||||
right 16px
|
||||
font-weight bold
|
||||
color $theme-color-foreground
|
||||
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
||||
border solid 1px lighten($theme-color, 15%)
|
||||
|
||||
&:hover
|
||||
background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
|
||||
border-color $theme-color
|
||||
|
||||
&:active
|
||||
background $theme-color
|
||||
border-color $theme-color
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<mk-window ref="window" is-modal @closed="$destroy">
|
||||
<span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-repost-form-window.title%</span>
|
||||
<mk-repost-form ref="form" :post="post" @posted="onPosted" @canceled="onCanceled"/>
|
||||
<span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-renote-form-window.title%</span>
|
||||
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
|
||||
</mk-window>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
props: ['note'],
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onDocumentKeydown);
|
||||
},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="mk-repost-form">
|
||||
<mk-post-preview :post="post"/>
|
||||
<div class="mk-renote-form">
|
||||
<mk-note-preview :note="note"/>
|
||||
<template v-if="!quote">
|
||||
<footer>
|
||||
<a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-repost-form.quote%</a>
|
||||
<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-repost-form.cancel%</button>
|
||||
<button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }}</button>
|
||||
<a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-renote-form.quote%</a>
|
||||
<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-renote-form.cancel%</button>
|
||||
<button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-renote-form.reposting%' : '%i18n:desktop.tags.mk-renote-form.renote%' }}</button>
|
||||
</footer>
|
||||
</template>
|
||||
<template v-if="quote">
|
||||
<mk-post-form ref="form" :repost="post" @posted="onChildFormPosted"/>
|
||||
<mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,7 +18,7 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
props: ['note'],
|
||||
data() {
|
||||
return {
|
||||
wait: false,
|
||||
@@ -28,13 +28,13 @@ export default Vue.extend({
|
||||
methods: {
|
||||
ok() {
|
||||
this.wait = true;
|
||||
(this as any).api('posts/create', {
|
||||
repostId: this.post.id
|
||||
(this as any).api('notes/create', {
|
||||
renoteId: this.note.id
|
||||
}).then(data => {
|
||||
this.$emit('posted');
|
||||
(this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.success%');
|
||||
(this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.success%');
|
||||
}).catch(err => {
|
||||
(this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.failure%');
|
||||
(this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.failure%');
|
||||
}).then(() => {
|
||||
this.wait = false;
|
||||
});
|
||||
@@ -59,9 +59,9 @@ export default Vue.extend({
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.mk-repost-form
|
||||
.mk-renote-form
|
||||
|
||||
> .mk-post-preview
|
||||
> .mk-note-preview
|
||||
margin 16px 22px
|
||||
|
||||
> footer
|
||||
|
||||
44
src/client/app/desktop/views/components/sub-note-content.vue
Normal file
44
src/client/app/desktop/views/components/sub-note-content.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="mk-sub-note-content">
|
||||
<div class="body">
|
||||
<a class="reply" v-if="note.replyId">%fa:reply%</a>
|
||||
<mk-note-html :text="note.text" :i="os.i"/>
|
||||
<a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a>
|
||||
</div>
|
||||
<details v-if="note.media.length > 0">
|
||||
<summary>({{ note.media.length }}つのメディア)</summary>
|
||||
<mk-media-list :media-list="note.media"/>
|
||||
</details>
|
||||
<details v-if="note.poll">
|
||||
<summary>投票</summary>
|
||||
<mk-poll :note="note"/>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['note']
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-sub-note-content
|
||||
overflow-wrap break-word
|
||||
|
||||
> .body
|
||||
> .reply
|
||||
margin-right 6px
|
||||
color #717171
|
||||
|
||||
> .rp
|
||||
margin-left 4px
|
||||
font-style oblique
|
||||
color #a0bf46
|
||||
|
||||
mk-poll
|
||||
font-size 80%
|
||||
|
||||
</style>
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div class="mk-sub-post-content">
|
||||
<div class="body">
|
||||
<a class="reply" v-if="post.replyId">%fa:reply%</a>
|
||||
<mk-post-html :text="post.text" :i="os.i"/>
|
||||
<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a>
|
||||
</div>
|
||||
<details v-if="post.media.length > 0">
|
||||
<summary>({{ post.media.length }}つのメディア)</summary>
|
||||
<mk-media-list :media-list="post.media"/>
|
||||
</details>
|
||||
<details v-if="post.poll">
|
||||
<summary>投票</summary>
|
||||
<mk-poll :post="post"/>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post']
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-sub-post-content
|
||||
overflow-wrap break-word
|
||||
|
||||
> .body
|
||||
> .reply
|
||||
margin-right 6px
|
||||
color #717171
|
||||
|
||||
> .rp
|
||||
margin-left 4px
|
||||
font-style oblique
|
||||
color #a0bf46
|
||||
|
||||
mk-poll
|
||||
font-size 80%
|
||||
|
||||
</style>
|
||||
@@ -4,15 +4,15 @@
|
||||
<div class="fetching" v-if="fetching">
|
||||
<mk-ellipsis-icon/>
|
||||
</div>
|
||||
<p class="empty" v-if="posts.length == 0 && !fetching">
|
||||
<p class="empty" v-if="notes.length == 0 && !fetching">
|
||||
%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。
|
||||
</p>
|
||||
<mk-posts :posts="posts" ref="timeline">
|
||||
<mk-notes :notes="notes" ref="timeline">
|
||||
<button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<template v-if="!moreFetching">もっと見る</template>
|
||||
<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
|
||||
</button>
|
||||
</mk-posts>
|
||||
</mk-notes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default Vue.extend({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
existMore: false,
|
||||
posts: [],
|
||||
notes: [],
|
||||
connection: null,
|
||||
connectionId: null,
|
||||
date: null
|
||||
@@ -41,7 +41,7 @@ export default Vue.extend({
|
||||
this.connection = (this as any).os.stream.getConnection();
|
||||
this.connectionId = (this as any).os.stream.use();
|
||||
|
||||
this.connection.on('post', this.onPost);
|
||||
this.connection.on('note', this.onNote);
|
||||
this.connection.on('follow', this.onChangeFollowing);
|
||||
this.connection.on('unfollow', this.onChangeFollowing);
|
||||
|
||||
@@ -51,7 +51,7 @@ export default Vue.extend({
|
||||
this.fetch();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.connection.off('post', this.onPost);
|
||||
this.connection.off('note', this.onNote);
|
||||
this.connection.off('follow', this.onChangeFollowing);
|
||||
this.connection.off('unfollow', this.onChangeFollowing);
|
||||
(this as any).os.stream.dispose(this.connectionId);
|
||||
@@ -63,45 +63,45 @@ export default Vue.extend({
|
||||
fetch(cb?) {
|
||||
this.fetching = true;
|
||||
|
||||
(this as any).api('posts/timeline', {
|
||||
(this as any).api('notes/timeline', {
|
||||
limit: 11,
|
||||
untilDate: this.date ? this.date.getTime() : undefined
|
||||
}).then(posts => {
|
||||
if (posts.length == 11) {
|
||||
posts.pop();
|
||||
}).then(notes => {
|
||||
if (notes.length == 11) {
|
||||
notes.pop();
|
||||
this.existMore = true;
|
||||
}
|
||||
this.posts = posts;
|
||||
this.notes = notes;
|
||||
this.fetching = false;
|
||||
this.$emit('loaded');
|
||||
if (cb) cb();
|
||||
});
|
||||
},
|
||||
more() {
|
||||
if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return;
|
||||
if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return;
|
||||
this.moreFetching = true;
|
||||
(this as any).api('posts/timeline', {
|
||||
(this as any).api('notes/timeline', {
|
||||
limit: 11,
|
||||
untilId: this.posts[this.posts.length - 1].id
|
||||
}).then(posts => {
|
||||
if (posts.length == 11) {
|
||||
posts.pop();
|
||||
untilId: this.notes[this.notes.length - 1].id
|
||||
}).then(notes => {
|
||||
if (notes.length == 11) {
|
||||
notes.pop();
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
this.posts = this.posts.concat(posts);
|
||||
this.notes = this.notes.concat(notes);
|
||||
this.moreFetching = false;
|
||||
});
|
||||
},
|
||||
onPost(post) {
|
||||
onNote(note) {
|
||||
// サウンドを再生する
|
||||
if ((this as any).os.isEnableSounds) {
|
||||
const sound = new Audio(`${url}/assets/post.mp3`);
|
||||
const sound = new Audio(`${url}/assets/note.mp3`);
|
||||
sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
|
||||
sound.play();
|
||||
}
|
||||
|
||||
this.posts.unshift(post);
|
||||
this.notes.unshift(note);
|
||||
},
|
||||
onChangeFollowing() {
|
||||
this.fetch();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="post">
|
||||
<button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button>
|
||||
<div class="note">
|
||||
<button @click="post" title="%i18n:desktop.tags.mk-ui-header-note-button.note%">%fa:pencil-alt%</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default Vue.extend({
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.post
|
||||
.note
|
||||
display inline-block
|
||||
padding 8px
|
||||
height 100%
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="description">{{ u.description }}</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<p>投稿</p><a>{{ u.postsCount }}</a>
|
||||
<p>投稿</p><a>{{ u.notesCount }}</a>
|
||||
</div>
|
||||
<div>
|
||||
<p>フォロー</p><a>{{ u.followingCount }}</a>
|
||||
|
||||
Reference in New Issue
Block a user