Merge branch 'develop'
This commit is contained in:
@ -144,8 +144,6 @@ export default prop => ({
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit(`update:${prop}`, this.$_ns_note_);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
171
src/client/app/common/scripts/paging.ts
Normal file
171
src/client/app/common/scripts/paging.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
export default (opts) => ({
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
queue: [],
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
inited: false,
|
||||
more: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
empty(): boolean {
|
||||
return this.items.length == 0 && !this.fetching && this.inited;
|
||||
},
|
||||
|
||||
error(): boolean {
|
||||
return !this.fetching && !this.inited;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
queue(x) {
|
||||
if (opts.onQueueChanged) opts.onQueueChanged(this, x);
|
||||
},
|
||||
|
||||
pagination() {
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
opts.displayLimit = opts.displayLimit || 30;
|
||||
this.init();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (opts.captureWindowScroll) {
|
||||
this.isScrollTop = () => {
|
||||
return window.scrollY <= 8;
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', this.onWindowScroll, { passive: true });
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (opts.captureWindowScroll) {
|
||||
window.removeEventListener('scroll', this.onWindowScroll);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateItem(i, item) {
|
||||
Vue.set((this as any).items, i, item);
|
||||
},
|
||||
|
||||
reload() {
|
||||
this.queue = [];
|
||||
this.items = [];
|
||||
this.init();
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params;
|
||||
if (params && params.then) params = await params;
|
||||
await this.$root.api(this.pagination.endpoint, {
|
||||
limit: (this.pagination.limit || 10) + 1,
|
||||
...params
|
||||
}).then(x => {
|
||||
if (x.length == (this.pagination.limit || 10) + 1) {
|
||||
x.pop();
|
||||
this.items = x;
|
||||
this.more = true;
|
||||
} else {
|
||||
this.items = x;
|
||||
this.more = false;
|
||||
}
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
if (opts.onInited) opts.onInited(this);
|
||||
}, e => {
|
||||
this.fetching = false;
|
||||
if (opts.onInited) opts.onInited(this);
|
||||
});
|
||||
},
|
||||
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.items.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
|
||||
if (params && params.then) params = await params;
|
||||
await this.$root.api(this.pagination.endpoint, {
|
||||
limit: (this.pagination.limit || 10) + 1,
|
||||
untilId: this.items[this.items.length - 1].id,
|
||||
...params
|
||||
}).then(x => {
|
||||
if (x.length == (this.pagination.limit || 10) + 1) {
|
||||
x.pop();
|
||||
this.items = this.items.concat(x);
|
||||
this.more = true;
|
||||
} else {
|
||||
this.items = this.items.concat(x);
|
||||
this.more = false;
|
||||
}
|
||||
this.moreFetching = false;
|
||||
}, e => {
|
||||
this.moreFetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
prepend(item, silent = false) {
|
||||
if (opts.onPrepend) {
|
||||
const cancel = opts.onPrepend(this, item, silent);
|
||||
if (cancel) return;
|
||||
}
|
||||
|
||||
if (this.isScrollTop == null || this.isScrollTop()) {
|
||||
// Prepend the item
|
||||
this.items.unshift(item);
|
||||
|
||||
// オーバーフローしたら古い投稿は捨てる
|
||||
if (this.items.length >= opts.displayLimit) {
|
||||
this.items = this.items.slice(0, opts.displayLimit);
|
||||
this.more = true;
|
||||
}
|
||||
} else {
|
||||
this.queue.push(item);
|
||||
}
|
||||
},
|
||||
|
||||
append(item) {
|
||||
this.items.push(item);
|
||||
},
|
||||
|
||||
releaseQueue() {
|
||||
for (const n of this.queue) {
|
||||
this.prepend(n, true);
|
||||
}
|
||||
this.queue = [];
|
||||
},
|
||||
|
||||
onWindowScroll() {
|
||||
if (this.isScrollTop()) {
|
||||
this.onTop();
|
||||
}
|
||||
|
||||
if (this.$store.state.settings.fetchOnScroll) {
|
||||
// 親要素が display none だったら弾く
|
||||
// https://github.com/syuilo/misskey/issues/1569
|
||||
// http://d.hatena.ne.jp/favril/20091105/1257403319
|
||||
if (this.$el.offsetHeight == 0) return;
|
||||
|
||||
const current = window.scrollY + window.innerHeight;
|
||||
if (current > document.body.offsetHeight - 8) this.onBottom();
|
||||
}
|
||||
},
|
||||
|
||||
onTop() {
|
||||
this.releaseQueue();
|
||||
},
|
||||
|
||||
onBottom() {
|
||||
this.fetchMore();
|
||||
}
|
||||
}
|
||||
});
|
27
src/client/app/common/views/components/avatars.vue
Normal file
27
src/client/app/common/views/components/avatars.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-avatar v-for="user in us" :user="user" :key="user.id" style="width:32px;height:32px;"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
userIds: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
us: []
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.us = await this.$root.api('users/show', {
|
||||
userIds: this.userIds
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
@ -19,7 +19,6 @@ export default Vue.extend({
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.a
|
||||
display block
|
||||
|
@ -21,7 +21,6 @@ import avatar from './avatar.vue';
|
||||
import nav from './nav.vue';
|
||||
import misskeyFlavoredMarkdown from './misskey-flavored-markdown.vue';
|
||||
import poll from './poll.vue';
|
||||
import pollEditor from './poll-editor.vue';
|
||||
import reactionIcon from './reaction-icon.vue';
|
||||
import reactionsViewer from './reactions-viewer.vue';
|
||||
import time from './time.vue';
|
||||
@ -46,6 +45,7 @@ import uiSelect from './ui/select.vue';
|
||||
import uiInfo from './ui/info.vue';
|
||||
import uiMargin from './ui/margin.vue';
|
||||
import uiHr from './ui/hr.vue';
|
||||
import uiPagination from './ui/pagination.vue';
|
||||
import formButton from './ui/form/button.vue';
|
||||
import formRadio from './ui/form/radio.vue';
|
||||
|
||||
@ -70,7 +70,6 @@ Vue.component('mk-acct', acct);
|
||||
Vue.component('mk-avatar', avatar);
|
||||
Vue.component('mk-nav', nav);
|
||||
Vue.component('mk-poll', poll);
|
||||
Vue.component('mk-poll-editor', pollEditor);
|
||||
Vue.component('mk-reaction-icon', reactionIcon);
|
||||
Vue.component('mk-reactions-viewer', reactionsViewer);
|
||||
Vue.component('mk-time', time);
|
||||
@ -95,5 +94,6 @@ Vue.component('ui-select', uiSelect);
|
||||
Vue.component('ui-info', uiInfo);
|
||||
Vue.component('ui-margin', uiMargin);
|
||||
Vue.component('ui-hr', uiHr);
|
||||
Vue.component('ui-pagination', uiPagination);
|
||||
Vue.component('form-button', formButton);
|
||||
Vue.component('form-radio', formRadio);
|
||||
|
@ -21,42 +21,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="history" v-if="messages.length > 0">
|
||||
<div class="title">{{ $t('user') }}</div>
|
||||
<a v-for="message in messages"
|
||||
class="user"
|
||||
:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
|
||||
:href="message.groupId ? `/i/messaging/group/${message.groupId}` : `/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
|
||||
:data-is-me="isMe(message)"
|
||||
:data-is-read="message.isRead"
|
||||
@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
|
||||
:data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead"
|
||||
@click.prevent="message.groupId ? navigateGroup(message.group) : navigate(isMe(message) ? message.recipient : message.user)"
|
||||
:key="message.id"
|
||||
>
|
||||
<div>
|
||||
<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/>
|
||||
<header>
|
||||
<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
|
||||
<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
|
||||
<mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
|
||||
<header v-if="message.groupId">
|
||||
<span class="name">{{ message.group.name }}</span>
|
||||
<mk-time :time="message.createdAt"/>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="history" v-if="groupMessages.length > 0">
|
||||
<div class="title">{{ $t('group') }}</div>
|
||||
<a v-for="message in groupMessages"
|
||||
class="user"
|
||||
:href="`/i/messaging/group/${message.groupId}`"
|
||||
:data-is-me="isMe(message)"
|
||||
:data-is-read="message.reads.includes($store.state.i.id)"
|
||||
@click.prevent="navigateGroup(message.group)"
|
||||
:key="message.id"
|
||||
>
|
||||
<div>
|
||||
<mk-avatar class="avatar" :user="message.user"/>
|
||||
<header>
|
||||
<span class="name">{{ message.group.name }}</span>
|
||||
<header v-else>
|
||||
<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
|
||||
<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
|
||||
<mk-time :time="message.createdAt"/>
|
||||
</header>
|
||||
<div class="body">
|
||||
@ -97,7 +78,6 @@ export default Vue.extend({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
messages: [],
|
||||
groupMessages: [],
|
||||
q: null,
|
||||
result: [],
|
||||
connection: null,
|
||||
@ -110,10 +90,11 @@ export default Vue.extend({
|
||||
this.connection.on('message', this.onMessage);
|
||||
this.connection.on('read', this.onRead);
|
||||
|
||||
this.$root.api('messaging/history', { group: false }).then(messages => {
|
||||
this.$root.api('messaging/history', { group: false }).then(userMessages => {
|
||||
this.$root.api('messaging/history', { group: true }).then(groupMessages => {
|
||||
const messages = userMessages.concat(groupMessages);
|
||||
messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
this.messages = messages;
|
||||
this.groupMessages = groupMessages;
|
||||
this.fetching = false;
|
||||
});
|
||||
});
|
||||
@ -134,8 +115,8 @@ export default Vue.extend({
|
||||
|
||||
this.messages.unshift(message);
|
||||
} else if (message.groupId) {
|
||||
this.groupMessages = this.groupMessages.filter(m => m.groupId !== message.groupId);
|
||||
this.groupMessages.unshift(message);
|
||||
this.messages = this.messages.filter(m => m.groupId !== message.groupId);
|
||||
this.messages.unshift(message);
|
||||
}
|
||||
},
|
||||
onRead(ids) {
|
||||
@ -243,9 +224,6 @@ export default Vue.extend({
|
||||
font-size 0.8em
|
||||
|
||||
> .history
|
||||
> .title
|
||||
padding 8px
|
||||
|
||||
> a
|
||||
&:last-child
|
||||
border-bottom none
|
||||
@ -384,14 +362,6 @@ export default Vue.extend({
|
||||
color rgba(#000, 0.3)
|
||||
|
||||
> .history
|
||||
> .title
|
||||
padding 6px 16px
|
||||
margin 0 auto
|
||||
max-width 500px
|
||||
background rgba(0, 0, 0, 0.05)
|
||||
color var(--text)
|
||||
font-size 85%
|
||||
|
||||
> a
|
||||
display block
|
||||
text-decoration none
|
||||
|
@ -29,7 +29,7 @@ export default Vue.extend({
|
||||
ToSUrl: null
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
mounted() {
|
||||
this.$root.getMeta(true).then(meta => {
|
||||
this.repositoryUrl = meta.repositoryUrl;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mk-poll-editor">
|
||||
<div class="zmdxowus">
|
||||
<p class="caution" v-if="choices.length < 2">
|
||||
<fa icon="exclamation-triangle"/>{{ $t('no-only-one-choice') }}
|
||||
</p>
|
||||
@ -134,7 +134,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-poll-editor
|
||||
.zmdxowus
|
||||
padding 8px
|
||||
|
||||
> .caution
|
||||
|
36
src/client/app/common/views/components/ui/pagination.vue
Normal file
36
src/client/app/common/views/components/ui/pagination.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="mwermpua" v-if="!fetching">
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<slot :items="items"></slot>
|
||||
</sequential-entrance>
|
||||
<div class="more" v-if="more">
|
||||
<ui-button @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import paging from '../../../scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
mixins: [
|
||||
paging({
|
||||
captureWindowScroll: false,
|
||||
}),
|
||||
],
|
||||
|
||||
props: {
|
||||
pagination: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mwermpua
|
||||
> .more
|
||||
margin-top 16px
|
||||
|
||||
</style>
|
@ -2,13 +2,13 @@
|
||||
<ui-container :body-togglable="true">
|
||||
<template #header><slot></slot></template>
|
||||
|
||||
<mk-error v-if="!fetching && !inited" @retry="init()"/>
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
|
||||
<div class="efvhhmdq" :class="{ iconOnly }" v-size="[{ lt: 500, class: 'narrow' }]">
|
||||
<div class="no-users" v-if="inited && us.length == 0">
|
||||
<div class="no-users" v-if="empty">
|
||||
<p>{{ $t('no-users') }}</p>
|
||||
</div>
|
||||
<div class="user" v-for="user in us" :key="user.id">
|
||||
<div class="user" v-for="user in users" :key="user.id">
|
||||
<mk-avatar class="avatar" :user="user"/>
|
||||
<div class="body" v-if="!iconOnly">
|
||||
<div class="name">
|
||||
@ -21,8 +21,8 @@
|
||||
<mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
|
||||
</div>
|
||||
</div>
|
||||
<button class="more" :class="{ fetching: fetchingMoreUsers }" v-if="cursor != null" @click="fetchMoreUsers()" :disabled="fetchingMoreUsers">
|
||||
<template v-if="fetchingMoreUsers"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreUsers ? $t('@.loading') : $t('@.load-more') }}
|
||||
<button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMoreUsers()" :disabled="moreFetching">
|
||||
<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
|
||||
</button>
|
||||
</div>
|
||||
</ui-container>
|
||||
@ -31,60 +31,31 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/user-list.vue'),
|
||||
|
||||
mixins: [
|
||||
paging({}),
|
||||
],
|
||||
|
||||
props: {
|
||||
makePromise: {
|
||||
pagination: {
|
||||
required: true
|
||||
},
|
||||
extract: {
|
||||
required: false
|
||||
},
|
||||
iconOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
fetchingMoreUsers: false,
|
||||
us: [],
|
||||
inited: false,
|
||||
more: false
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.us = x;
|
||||
} else {
|
||||
this.us = x.users;
|
||||
this.cursor = x.cursor;
|
||||
}
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
}, e => {
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
async fetchMoreUsers() {
|
||||
this.fetchingMoreUsers = true;
|
||||
await (this.makePromise(this.cursor)).then(x => {
|
||||
this.us = this.us.concat(x.users);
|
||||
this.cursor = x.cursor;
|
||||
this.fetchingMoreUsers = false;
|
||||
}, e => {
|
||||
this.fetchingMoreUsers = false;
|
||||
});
|
||||
computed: {
|
||||
users() {
|
||||
return this.extract ? this.extract(this.items) : this.items;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
<x-column :name="name" :column="column" :is-stacked="isStacked">
|
||||
<template #header><fa :icon="['far', 'envelope']"/>{{ name }}</template>
|
||||
|
||||
<x-direct/>
|
||||
<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
@ -10,13 +10,14 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XDirect from './deck.direct.vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
components: {
|
||||
XColumn,
|
||||
XDirect
|
||||
XNotes
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -30,6 +31,22 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
pagination: {
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
params: {
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
|
||||
visibility: 'specified'
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
name(): string {
|
||||
if (this.column.name) return this.column.name;
|
||||
@ -37,9 +54,25 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection.on('mention', this.onNote);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
onNote(note) {
|
||||
// Prepend a note
|
||||
if (note.visibility == 'specified') {
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
}
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$refs.tl.focus();
|
||||
this.$refs.timeline.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XNotes
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
makePromise: cursor => this.$root.api('notes/mentions', {
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
|
||||
visibility: 'specified'
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection.on('mention', this.onNote);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
onNote(note) {
|
||||
// Prepend a note
|
||||
if (note.visibility == 'specified') {
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
}
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$refs.timeline.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<x-column>
|
||||
<template #header>
|
||||
<fa :icon="['fa', 'star']"/>{{ $t('@.favorites') }}
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
</div>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
components: {
|
||||
XColumn,
|
||||
XNotes,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('i/favorites', {
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
}).then(notes => {
|
||||
notes = notes.map(x => x.note);
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.timeline.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<x-column>
|
||||
<template #header>
|
||||
<fa :icon="faNewspaper"/>{{ $t('@.featured-notes') }}
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
</div>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
import { faNewspaper } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
components: {
|
||||
XColumn,
|
||||
XNotes,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faNewspaper,
|
||||
makePromise: cursor => this.$root.api('notes/featured', {
|
||||
limit: 30,
|
||||
}).then(notes => {
|
||||
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
return notes;
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.timeline.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XNotes
|
||||
@ -28,28 +26,18 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
makePromise: cursor => this.$root.api('notes/search-by-tag', {
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
|
||||
query: this.tagTl.query
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'notes/search-by-tag',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
|
||||
query: this.tagTl.query
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XNotes
|
||||
@ -28,28 +26,18 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
makePromise: cursor => this.$root.api('notes/user-list-timeline', {
|
||||
listId: this.list.id,
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'notes/user-list-timeline',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
listId: this.list.id,
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<x-column :name="name" :column="column" :is-stacked="isStacked">
|
||||
<template #header><fa icon="at"/>{{ name }}</template>
|
||||
|
||||
<x-mentions ref="tl"/>
|
||||
<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</x-column>
|
||||
</template>
|
||||
|
||||
@ -10,13 +10,14 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XMentions from './deck.mentions.vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
components: {
|
||||
XColumn,
|
||||
XMentions
|
||||
XNotes
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -30,6 +31,22 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
pagination: {
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
name(): string {
|
||||
if (this.column.name) return this.column.name;
|
||||
@ -37,9 +54,22 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection.on('mention', this.onNote);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
onNote(note) {
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$refs.tl.focus();
|
||||
this.$refs.timeline.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,61 +0,0 @@
|
||||
<template>
|
||||
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XNotes
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
makePromise: cursor => this.$root.api('notes/mentions', {
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
this.connection.on('mention', this.onNote);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
onNote(note) {
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$refs.timeline.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu">
|
||||
<div class="empty" v-if="notes.length == 0 && !fetching && inited">{{ $t('@.no-notes') }}</div>
|
||||
<div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div>
|
||||
|
||||
<mk-error v-if="!fetching && !inited" @retry="init()"/>
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
|
||||
<div class="placeholder" v-if="fetching">
|
||||
<template v-for="i in 10">
|
||||
@ -16,7 +16,6 @@
|
||||
<mk-note
|
||||
:note="note"
|
||||
:key="note.id"
|
||||
@update:note="onNoteUpdated(i, $event)"
|
||||
:compact="true"
|
||||
/>
|
||||
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||
@ -39,33 +38,47 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import shouldMuteNote from '../../../common/scripts/should-mute-note';
|
||||
|
||||
const displayLimit = 20;
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
inject: ['column', 'isScrollTop', 'count'],
|
||||
|
||||
mixins: [
|
||||
paging({
|
||||
limit: 20,
|
||||
|
||||
onQueueChanged: (self, q) => {
|
||||
self.count(q.length);
|
||||
},
|
||||
|
||||
onPrepend: (self, note, silent) => {
|
||||
// 弾く
|
||||
if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false;
|
||||
|
||||
// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
|
||||
if (document.hidden || !self.isScrollTop()) {
|
||||
self.$store.commit('pushBehindNote', note);
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
||||
props: {
|
||||
makePromise: {
|
||||
pagination: {
|
||||
required: true
|
||||
},
|
||||
extract: {
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
rootEl: null,
|
||||
notes: [],
|
||||
queue: [],
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
inited: false,
|
||||
more: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
notes() {
|
||||
return this.extract ? this.extract(this.items) : this.items;
|
||||
},
|
||||
|
||||
_notes(): any[] {
|
||||
return (this.notes as any).map(note => {
|
||||
const date = new Date(note.createdAt).getDate();
|
||||
@ -77,15 +90,6 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
queue(q) {
|
||||
this.count(q.length);
|
||||
},
|
||||
makePromise() {
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.column.$on('top', this.onTop);
|
||||
this.column.$on('bottom', this.onBottom);
|
||||
@ -101,88 +105,6 @@ export default Vue.extend({
|
||||
focus() {
|
||||
(this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
|
||||
},
|
||||
|
||||
onNoteUpdated(i, note) {
|
||||
Vue.set((this as any).notes, i, note);
|
||||
},
|
||||
|
||||
reload() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.queue = [];
|
||||
this.notes = [];
|
||||
this.fetching = true;
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
this.notes = x.notes;
|
||||
this.more = x.more;
|
||||
}
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
this.$emit('inited');
|
||||
}, e => {
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching) return;
|
||||
this.moreFetching = true;
|
||||
await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
}, e => {
|
||||
this.moreFetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
prepend(note, silent = false) {
|
||||
// 弾く
|
||||
if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return;
|
||||
|
||||
// タブが非表示ならタイトルで通知
|
||||
if (document.hidden) {
|
||||
this.$store.commit('pushBehindNote', note);
|
||||
}
|
||||
|
||||
if (this.isScrollTop()) {
|
||||
// Prepend the note
|
||||
this.notes.unshift(note);
|
||||
|
||||
// オーバーフローしたら古い投稿は捨てる
|
||||
if (this.notes.length >= displayLimit) {
|
||||
this.notes = this.notes.slice(0, displayLimit);
|
||||
this.more = true;
|
||||
}
|
||||
} else {
|
||||
this.queue.push(note);
|
||||
}
|
||||
},
|
||||
|
||||
append(note) {
|
||||
this.notes.push(note);
|
||||
this.cursor = this.notes[this.notes.length - 1].id
|
||||
},
|
||||
|
||||
releaseQueue() {
|
||||
for (const n of this.queue) {
|
||||
this.prepend(n, true);
|
||||
}
|
||||
this.queue = [];
|
||||
},
|
||||
|
||||
onTop() {
|
||||
this.releaseQueue();
|
||||
},
|
||||
|
||||
onBottom() {
|
||||
this.fetchMore();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -81,15 +81,15 @@
|
||||
</div>
|
||||
|
||||
<template v-if="notification.type == 'quote'">
|
||||
<mk-note :note="notification.note" @update:note="onNoteUpdated"/>
|
||||
<mk-note :note="notification.note"/>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type == 'reply'">
|
||||
<mk-note :note="notification.note" @update:note="onNoteUpdated"/>
|
||||
<mk-note :note="notification.note"/>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type == 'mention'">
|
||||
<mk-note :note="notification.note" @update:note="onNoteUpdated"/>
|
||||
<mk-note :note="notification.note"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -105,17 +105,6 @@ export default Vue.extend({
|
||||
getNoteSummary
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onNoteUpdated(note) {
|
||||
switch (this.notification.type) {
|
||||
case 'quote':
|
||||
case 'reply':
|
||||
case 'mention':
|
||||
Vue.set(this.notification, 'note', note);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -10,16 +10,17 @@
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div">
|
||||
<template v-for="(notification, i) in _notifications">
|
||||
<x-notification class="notification" :notification="notification" :key="notification.id"/>
|
||||
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
|
||||
<p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
|
||||
<span><fa icon="angle-up"/>{{ notification._datetext }}</span>
|
||||
<span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</component>
|
||||
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
|
||||
<template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreNotifications ? this.$t('@.loading') : this.$t('@.load-more') }}
|
||||
<button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching">
|
||||
<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? this.$t('@.loading') : this.$t('@.load-more') }}
|
||||
</button>
|
||||
<p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p>
|
||||
<p class="empty" v-if="empty">{{ $t('empty') }}</p>
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -27,31 +28,38 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import XNotification from './deck.notification.vue';
|
||||
|
||||
const displayLimit = 20;
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
components: {
|
||||
XNotification
|
||||
},
|
||||
|
||||
inject: ['column', 'isScrollTop', 'count'],
|
||||
|
||||
mixins: [
|
||||
paging({
|
||||
onQueueChanged: (self, q) => {
|
||||
self.count(q.length);
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
fetchingMoreNotifications: false,
|
||||
notifications: [],
|
||||
queue: [],
|
||||
moreNotifications: false,
|
||||
connection: null
|
||||
connection: null,
|
||||
pagination: {
|
||||
endpoint: 'i/notifications',
|
||||
limit: 20,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
_notifications(): any[] {
|
||||
return (this.notifications as any).map(notification => {
|
||||
return (this.items as any).map(notification => {
|
||||
const date = new Date(notification.createdAt).getDate();
|
||||
const month = new Date(notification.createdAt).getMonth() + 1;
|
||||
notification._date = date;
|
||||
@ -61,33 +69,12 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
queue(q) {
|
||||
this.count(q.length);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
|
||||
this.connection.on('notification', this.onNotification);
|
||||
|
||||
this.column.$on('top', this.onTop);
|
||||
this.column.$on('bottom', this.onBottom);
|
||||
|
||||
const max = 10;
|
||||
|
||||
this.$root.api('i/notifications', {
|
||||
limit: max + 1
|
||||
}).then(notifications => {
|
||||
if (notifications.length == max + 1) {
|
||||
this.moreNotifications = true;
|
||||
notifications.pop();
|
||||
}
|
||||
|
||||
this.notifications = notifications;
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
@ -98,26 +85,6 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchMoreNotifications() {
|
||||
this.fetchingMoreNotifications = true;
|
||||
|
||||
const max = 20;
|
||||
|
||||
this.$root.api('i/notifications', {
|
||||
limit: max + 1,
|
||||
untilId: this.notifications[this.notifications.length - 1].id
|
||||
}).then(notifications => {
|
||||
if (notifications.length == max + 1) {
|
||||
this.moreNotifications = true;
|
||||
notifications.pop();
|
||||
} else {
|
||||
this.moreNotifications = false;
|
||||
}
|
||||
this.notifications = this.notifications.concat(notifications);
|
||||
this.fetchingMoreNotifications = false;
|
||||
});
|
||||
},
|
||||
|
||||
onNotification(notification) {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.$root.stream.send('readNotification', {
|
||||
@ -126,35 +93,6 @@ export default Vue.extend({
|
||||
|
||||
this.prepend(notification);
|
||||
},
|
||||
|
||||
prepend(notification) {
|
||||
if (this.isScrollTop()) {
|
||||
// Prepend the notification
|
||||
this.notifications.unshift(notification);
|
||||
|
||||
// オーバーフローしたら古い通知は捨てる
|
||||
if (this.notifications.length >= displayLimit) {
|
||||
this.notifications = this.notifications.slice(0, displayLimit);
|
||||
}
|
||||
} else {
|
||||
this.queue.push(notification);
|
||||
}
|
||||
},
|
||||
|
||||
releaseQueue() {
|
||||
for (const n of this.queue) {
|
||||
this.prepend(n);
|
||||
}
|
||||
this.queue = [];
|
||||
},
|
||||
|
||||
onTop() {
|
||||
this.releaseQueue();
|
||||
},
|
||||
|
||||
onBottom() {
|
||||
this.fetchMoreNotifications();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</div>
|
||||
</x-column>
|
||||
</template>
|
||||
@ -16,8 +16,6 @@ import XColumn from './deck.column.vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XColumn,
|
||||
@ -26,24 +24,11 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
return {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: cursor ? cursor + limit : limit
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'notes/search',
|
||||
limit: 20,
|
||||
params: () => genSearchQuery(this, this.q)
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
</p>
|
||||
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
||||
</div>
|
||||
<x-notes v-else ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
<x-notes v-else ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -15,8 +15,6 @@ import XNotes from './deck.notes.vue';
|
||||
import { faMinusCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('deck'),
|
||||
|
||||
@ -42,7 +40,7 @@ export default Vue.extend({
|
||||
connection: null,
|
||||
disabled: false,
|
||||
faMinusCircle,
|
||||
makePromise: null
|
||||
pagination: null
|
||||
};
|
||||
},
|
||||
|
||||
@ -73,27 +71,17 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
created() {
|
||||
this.makePromise = cursor => this.$root.api(this.endpoint, {
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
});
|
||||
this.pagination = {
|
||||
endpoint: this.endpoint,
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
withFiles: this.mediaOnly,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -30,7 +30,7 @@
|
||||
<ui-container>
|
||||
<template #header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</template>
|
||||
<div>
|
||||
<x-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
@ -43,8 +43,6 @@ import XNotes from './deck.notes.vue';
|
||||
import { concat } from '../../../../../prelude/array';
|
||||
import ApexCharts from 'apexcharts';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('deck/deck.user-column.vue'),
|
||||
|
||||
@ -63,49 +61,38 @@ export default Vue.extend({
|
||||
return {
|
||||
withFiles: false,
|
||||
images: [],
|
||||
makePromise: null,
|
||||
chart: null as ApexCharts
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pagination() {
|
||||
return {
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
userId: this.user.id,
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
withFiles: this.withFiles,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
user() {
|
||||
this.fetch();
|
||||
this.genPromiseMaker();
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
this.genPromiseMaker();
|
||||
},
|
||||
|
||||
methods: {
|
||||
genPromiseMaker() {
|
||||
this.makePromise = cursor => this.$root.api('users/notes', {
|
||||
userId: this.user.id,
|
||||
limit: fetchLimit + 1,
|
||||
untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365,
|
||||
withFiles: this.withFiles,
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetch() {
|
||||
const image = [
|
||||
'image/jpeg',
|
||||
|
@ -18,24 +18,24 @@
|
||||
</div>
|
||||
</ui-container>
|
||||
|
||||
<mk-user-list v-if="tag != null" :make-promise="tagUsers" :key="`${tag}-local`">
|
||||
<mk-user-list v-if="tag != null" :pagination="tagUsers" :key="`${tag}-local`">
|
||||
<fa :icon="faHashtag" fixed-width/>{{ tag }}
|
||||
</mk-user-list>
|
||||
<mk-user-list v-if="tag != null" :make-promise="tagRemoteUsers" :key="`${tag}-remote`">
|
||||
<mk-user-list v-if="tag != null" :pagination="tagRemoteUsers" :key="`${tag}-remote`">
|
||||
<fa :icon="faHashtag" fixed-width/>{{ tag }} ({{ $t('federated') }})
|
||||
</mk-user-list>
|
||||
|
||||
<template v-if="tag == null">
|
||||
<mk-user-list :make-promise="pinnedUsers">
|
||||
<mk-user-list :pagination="pinnedUsers">
|
||||
<fa :icon="faBookmark" fixed-width/>{{ $t('pinned-users') }}
|
||||
</mk-user-list>
|
||||
<mk-user-list :make-promise="popularUsers">
|
||||
<mk-user-list :pagination="popularUsers">
|
||||
<fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }}
|
||||
</mk-user-list>
|
||||
<mk-user-list :make-promise="recentlyUpdatedUsers">
|
||||
<mk-user-list :pagination="recentlyUpdatedUsers">
|
||||
<fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }}
|
||||
</mk-user-list>
|
||||
<mk-user-list :make-promise="recentlyRegisteredUsers">
|
||||
<mk-user-list :pagination="recentlyRegisteredUsers">
|
||||
<fa :icon="faPlus" fixed-width/>{{ $t('recently-registered-users') }}
|
||||
</mk-user-list>
|
||||
</template>
|
||||
@ -60,24 +60,21 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
return {
|
||||
pinnedUsers: () => this.$root.api('pinned-users'),
|
||||
popularUsers: () => this.$root.api('users', {
|
||||
pinnedUsers: { endpoint: 'pinned-users' },
|
||||
popularUsers: { endpoint: 'users', limit: 10, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
limit: 10
|
||||
}),
|
||||
recentlyUpdatedUsers: () => this.$root.api('users', {
|
||||
} },
|
||||
recentlyUpdatedUsers: { endpoint: 'users', limit: 10, params: {
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
limit: 10
|
||||
}),
|
||||
recentlyRegisteredUsers: () => this.$root.api('users', {
|
||||
} },
|
||||
recentlyRegisteredUsers: { endpoint: 'users', limit: 10, params: {
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
limit: 10
|
||||
}),
|
||||
} },
|
||||
tagsLocal: [],
|
||||
tagsRemote: [],
|
||||
stats: null,
|
||||
@ -88,24 +85,30 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
computed: {
|
||||
tagUsers(): () => Promise<any> {
|
||||
return () => this.$root.api('hashtags/users', {
|
||||
tag: this.tag,
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
limit: 30
|
||||
});
|
||||
tagUsers(): any {
|
||||
return {
|
||||
endpoint: 'hashtags/users',
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: this.tag,
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
tagRemoteUsers(): () => Promise<any> {
|
||||
return () => this.$root.api('hashtags/users', {
|
||||
tag: this.tag,
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
limit: 30
|
||||
});
|
||||
tagRemoteUsers(): any {
|
||||
return {
|
||||
endpoint: 'hashtags/users',
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: this.tag,
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
|
44
src/client/app/common/views/pages/favorites.vue
Normal file
44
src/client/app/common/views/pages/favorites.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<component :is="notesComponent" :pagination="pagination" :extract="items => items.map(item => item.note)"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faStar } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../../../i18n';
|
||||
//import Progress from '../../../common/scripts/loading';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
props: {
|
||||
platform: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
pagination: {
|
||||
endpoint: 'i/favorites',
|
||||
limit: 10,
|
||||
},
|
||||
|
||||
notesComponent:
|
||||
this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) :
|
||||
this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) :
|
||||
this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$emit('init', {
|
||||
title: this.$t('@.favorites'),
|
||||
icon: faStar
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
44
src/client/app/common/views/pages/featured.vue
Normal file
44
src/client/app/common/views/pages/featured.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<component :is="notesComponent" :pagination="pagination"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faNewspaper } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../../../i18n';
|
||||
//import Progress from '../../../common/scripts/loading';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
props: {
|
||||
platform: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
pagination: {
|
||||
endpoint: 'notes/featured',
|
||||
limit: 29,
|
||||
},
|
||||
|
||||
notesComponent:
|
||||
this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) :
|
||||
this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) :
|
||||
this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$emit('init', {
|
||||
title: this.$t('@.featured-notes'),
|
||||
icon: faNewspaper
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-user-list :make-promise="makePromise">{{ $t('@.followers') }}</mk-user-list>
|
||||
</div>
|
||||
<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.follower)">{{ $t('@.followers') }}</mk-user-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -9,31 +7,18 @@ import Vue from 'vue';
|
||||
import parseAcct from '../../../../../misc/acct/parse';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
const fetchLimit = 30;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('users/followers', {
|
||||
...parseAcct(this.$route.params.user),
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
}).then(followings => {
|
||||
if (followings.length == fetchLimit + 1) {
|
||||
followings.pop();
|
||||
return {
|
||||
users: followings.map(following => following.follower),
|
||||
cursor: followings[followings.length - 1].id
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
users: followings.map(following => following.follower),
|
||||
more: false
|
||||
};
|
||||
pagination: {
|
||||
endpoint: 'users/followers',
|
||||
limit: 30,
|
||||
params: {
|
||||
...parseAcct(this.$route.params.user),
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-user-list :make-promise="makePromise">{{ $t('@.following') }}</mk-user-list>
|
||||
</div>
|
||||
<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.followee)">{{ $t('@.following') }}</mk-user-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -9,31 +7,18 @@ import Vue from 'vue';
|
||||
import parseAcct from '../../../../../misc/acct/parse';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
const fetchLimit = 30;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('users/following', {
|
||||
...parseAcct(this.$route.params.user),
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
}).then(followings => {
|
||||
if (followings.length == fetchLimit + 1) {
|
||||
followings.pop();
|
||||
return {
|
||||
users: followings.map(following => following.followee),
|
||||
cursor: followings[followings.length - 1].id
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
users: followings.map(following => following.followee),
|
||||
more: false
|
||||
};
|
||||
pagination: {
|
||||
endpoint: 'users/following',
|
||||
limit: 30,
|
||||
params: {
|
||||
...parseAcct(this.$route.params.user),
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -2,22 +2,20 @@
|
||||
<div>
|
||||
<ui-container :body-togglable="true">
|
||||
<template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template>
|
||||
<div class="rknalgpo" v-if="!fetching">
|
||||
<div class="rknalgpo my">
|
||||
<ui-button class="new" @click="create()"><fa :icon="faPlus"/></ui-button>
|
||||
<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages">
|
||||
<x-page-preview v-for="page in pages" class="page" :page="page" :key="page.id"/>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="existMore" @click="fetchMore()" style="margin-top:16px;">{{ $t('@.load-more') }}</ui-button>
|
||||
<ui-pagination :pagination="myPagesPagination" #default="{items}">
|
||||
<x-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</ui-container>
|
||||
|
||||
<ui-container :body-togglable="true">
|
||||
<template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template>
|
||||
<div class="rknalgpo" v-if="!fetching">
|
||||
<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages">
|
||||
<x-page-preview v-for="like in likes" class="page" :page="like.page" :key="like.page.id"/>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="existMoreLikes" @click="fetchMoreLiked()">{{ $t('@.load-more') }}</ui-button>
|
||||
<div class="rknalgpo">
|
||||
<ui-pagination :pagination="likedPagesPagination" #default="{items}">
|
||||
<x-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
@ -28,7 +26,6 @@ import Vue from 'vue';
|
||||
import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../scripts/loading';
|
||||
import XPagePreview from '../../views/components/page-preview.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
@ -38,87 +35,24 @@ export default Vue.extend({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
pages: [],
|
||||
existMore: false,
|
||||
moreFetching: false,
|
||||
likes: [],
|
||||
existMoreLikes: false,
|
||||
moreLikesFetching: false,
|
||||
myPagesPagination: {
|
||||
endpoint: 'i/pages',
|
||||
limit: 5,
|
||||
},
|
||||
likedPagesPagination: {
|
||||
endpoint: 'i/page-likes',
|
||||
limit: 5,
|
||||
},
|
||||
faStickyNote, faPlus, faEdit, faHeart
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
|
||||
this.$emit('init', {
|
||||
title: this.$t('@.pages'),
|
||||
icon: faStickyNote
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async fetch() {
|
||||
Progress.start();
|
||||
this.fetching = true;
|
||||
|
||||
const pages = await this.$root.api('i/pages', {
|
||||
limit: 11
|
||||
});
|
||||
|
||||
if (pages.length == 11) {
|
||||
this.existMore = true;
|
||||
pages.pop();
|
||||
}
|
||||
|
||||
const likes = await this.$root.api('i/page-likes', {
|
||||
limit: 11
|
||||
});
|
||||
|
||||
if (likes.length == 11) {
|
||||
this.existMoreLikes = true;
|
||||
likes.pop();
|
||||
}
|
||||
|
||||
this.pages = pages;
|
||||
this.likes = likes;
|
||||
this.fetching = false;
|
||||
|
||||
Progress.done();
|
||||
},
|
||||
fetchMore() {
|
||||
this.moreFetching = true;
|
||||
this.$root.api('i/pages', {
|
||||
limit: 11,
|
||||
untilId: this.pages[this.pages.length - 1].id
|
||||
}).then(pages => {
|
||||
if (pages.length == 11) {
|
||||
this.existMore = true;
|
||||
pages.pop();
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
|
||||
this.pages = this.pages.concat(pages);
|
||||
this.moreFetching = false;
|
||||
});
|
||||
},
|
||||
fetchMoreLiked() {
|
||||
this.moreLikesFetching = true;
|
||||
this.$root.api('i/page-likes', {
|
||||
limit: 11,
|
||||
untilId: this.likes[this.likes.length - 1].id
|
||||
}).then(pages => {
|
||||
if (pages.length == 11) {
|
||||
this.existMoreLikes = true;
|
||||
pages.pop();
|
||||
} else {
|
||||
this.existMoreLikes = false;
|
||||
}
|
||||
|
||||
this.likes = this.likes.concat(pages);
|
||||
this.moreLikesFetching = false;
|
||||
});
|
||||
},
|
||||
create() {
|
||||
this.$router.push(`/i/pages/new`);
|
||||
}
|
||||
@ -130,14 +64,14 @@ export default Vue.extend({
|
||||
.rknalgpo
|
||||
padding 16px
|
||||
|
||||
> .new
|
||||
margin-bottom 16px
|
||||
&.my .ckltabjg:first-child
|
||||
margin-top 16px
|
||||
|
||||
> * > .page:not(:last-child)
|
||||
.ckltabjg:not(:last-child)
|
||||
margin-bottom 8px
|
||||
|
||||
@media (min-width 500px)
|
||||
> * > .page:not(:last-child)
|
||||
.ckltabjg:not(:last-child)
|
||||
margin-bottom 16px
|
||||
|
||||
</style>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<h1>{{ $t('share-with', { name }) }}</h1>
|
||||
<div>
|
||||
<mk-signin v-if="!$store.getters.isSignedIn"/>
|
||||
<mk-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/>
|
||||
<x-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/>
|
||||
<p v-if="posted" class="posted"><fa icon="check"/></p>
|
||||
</div>
|
||||
<ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button>
|
||||
@ -16,6 +16,9 @@ import i18n from '../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/pages/share.vue'),
|
||||
components: {
|
||||
XPostForm: () => import('../../../desktop/views/components/post-form.vue').then(m => m.default)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
@ -35,15 +38,15 @@ export default Vue.extend({
|
||||
return t.trim();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
window.close();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getMeta().then(meta => {
|
||||
this.name = meta.name || 'Misskey';
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-container>
|
||||
<ui-container :body-togglable="true">
|
||||
<template #header><fa :icon="faUsers"/> {{ $t('owned-groups') }}</template>
|
||||
<ui-margin>
|
||||
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button>
|
||||
@ -9,26 +9,29 @@
|
||||
<ui-hr/>
|
||||
<ui-margin>
|
||||
<router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link>
|
||||
<x-avatars :user-ids="group.userIds" style="margin-top:8px;"/>
|
||||
</ui-margin>
|
||||
</div>
|
||||
</ui-container>
|
||||
|
||||
<ui-container>
|
||||
<ui-container :body-togglable="true">
|
||||
<template #header><fa :icon="faUsers"/> {{ $t('joined-groups') }}</template>
|
||||
<div class="hwgkdrbl" v-for="(group, i) in joinedGroups" :key="group.id">
|
||||
<ui-hr v-if="i != 0"/>
|
||||
<ui-margin>
|
||||
<router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link>
|
||||
<div>{{ group.name }}</div>
|
||||
<x-avatars :user-ids="group.userIds" style="margin-top:8px;"/>
|
||||
</ui-margin>
|
||||
</div>
|
||||
</ui-container>
|
||||
|
||||
<ui-container>
|
||||
<ui-container :body-togglable="true">
|
||||
<template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template>
|
||||
<div class="fvlojuur" v-for="(invite, i) in invites" :key="invite.id">
|
||||
<ui-hr v-if="i != 0"/>
|
||||
<ui-margin>
|
||||
<div class="name">{{ invite.group.name }}</div>
|
||||
<x-avatars :user-ids="invite.group.userIds" style="margin-top:8px;"/>
|
||||
<ui-horizon-group>
|
||||
<ui-button @click="acceptInvite(invite)"><fa :icon="faCheck"/> {{ $t('accept-invite') }}</ui-button>
|
||||
<ui-button @click="rejectInvite(invite)"><fa :icon="faBan"/> {{ $t('reject-invite') }}</ui-button>
|
||||
@ -41,11 +44,15 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import { faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../../../i18n';
|
||||
import XAvatars from '../../views/components/avatars.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/user-groups.vue'),
|
||||
components: {
|
||||
XAvatars
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ownedGroups: [],
|
||||
|
@ -8,6 +8,7 @@
|
||||
<ui-hr/>
|
||||
<ui-margin>
|
||||
<router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link>
|
||||
<x-avatars :user-ids="list.userIds" style="margin-top:8px;"/>
|
||||
</ui-margin>
|
||||
</div>
|
||||
</ui-container>
|
||||
@ -17,9 +18,13 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import XAvatars from '../../views/components/avatars.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/user-lists.vue'),
|
||||
components: {
|
||||
XAvatars
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
|
@ -38,7 +38,6 @@
|
||||
import define from '../../../common/define-widget';
|
||||
import i18n from '../../../i18n';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import XPostFormAttaches from '../components/post-form-attaches.vue';
|
||||
|
||||
export default define({
|
||||
name: 'post-form',
|
||||
@ -49,7 +48,7 @@ export default define({
|
||||
i18n: i18n('desktop/views/widgets/post-form.vue'),
|
||||
|
||||
components: {
|
||||
XPostFormAttaches
|
||||
XPostFormAttaches: () => import('../components/post-form-attaches.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -13,7 +13,6 @@ import fuckAdBlock from '../common/scripts/fuck-ad-block';
|
||||
import composeNotification from '../common/scripts/compose-notification';
|
||||
|
||||
import MkHome from './views/home/home.vue';
|
||||
import MkDeck from '../common/views/deck/deck.vue';
|
||||
import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||
import MkDrive from './views/pages/drive.vue';
|
||||
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
||||
@ -25,7 +24,6 @@ import MkSettings from './views/pages/settings.vue';
|
||||
import DeckColumn from '../common/views/deck/deck.column-template.vue';
|
||||
|
||||
import Ctx from './views/components/context-menu.vue';
|
||||
import PostFormWindow from './views/components/post-form-window.vue';
|
||||
import RenoteFormWindow from './views/components/renote-form-window.vue';
|
||||
import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue';
|
||||
import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue';
|
||||
@ -62,12 +60,13 @@ init(async (launch, os) => {
|
||||
});
|
||||
if (o.cb) vm.$once('closed', o.cb);
|
||||
} else {
|
||||
const vm = this.$root.new(PostFormWindow, {
|
||||
this.$root.newAsync(() => import('./views/components/post-form-window.vue').then(m => m.default), {
|
||||
reply: o.reply,
|
||||
mention: o.mention,
|
||||
animation: o.animation == null ? true : o.animation
|
||||
}).then(vm => {
|
||||
if (o.cb) vm.$once('closed', o.cb);
|
||||
});
|
||||
if (o.cb) vm.$once('closed', o.cb);
|
||||
}
|
||||
},
|
||||
|
||||
@ -129,7 +128,7 @@ init(async (launch, os) => {
|
||||
mode: 'history',
|
||||
routes: [
|
||||
os.store.state.device.inDeckMode
|
||||
? { path: '/', name: 'index', component: MkDeck, children: [
|
||||
? { path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [
|
||||
{ path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [
|
||||
{ path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) },
|
||||
{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
|
||||
@ -138,10 +137,10 @@ init(async (launch, os) => {
|
||||
{ path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) },
|
||||
{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) },
|
||||
{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) },
|
||||
{ path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) },
|
||||
{ path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) },
|
||||
{ path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
|
||||
{ path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
|
||||
{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) },
|
||||
{ path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) },
|
||||
{ path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
|
||||
{ path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
|
||||
{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) },
|
||||
@ -158,10 +157,10 @@ init(async (launch, os) => {
|
||||
{ path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) },
|
||||
{ path: '/search', component: () => import('./views/home/search.vue').then(m => m.default) },
|
||||
{ path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) },
|
||||
{ path: '/featured', name: 'featured', component: () => import('./views/home/featured.vue').then(m => m.default) },
|
||||
{ path: '/featured', name: 'featured', component: () => import('../common/views/pages/featured.vue').then(m => m.default), props: { platform: 'desktop' } },
|
||||
{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
|
||||
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
|
||||
{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
|
||||
{ path: '/i/favorites', component: () => import('../common/views/pages/favorites.vue').then(m => m.default), props: { platform: 'desktop' } },
|
||||
{ path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) },
|
||||
{ path: '/i/lists', component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) },
|
||||
{ path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) },
|
||||
|
56
src/client/app/desktop/views/components/detail-notes.vue
Normal file
56
src/client/app/desktop/views/components/detail-notes.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="ecsvsegy" v-if="!fetching">
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<template v-for="note in notes">
|
||||
<mk-note-detail class="post" :note="note" :key="note.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
<div class="more" v-if="more">
|
||||
<ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
mixins: [
|
||||
paging({
|
||||
captureWindowScroll: true,
|
||||
}),
|
||||
],
|
||||
|
||||
props: {
|
||||
pagination: {
|
||||
required: true
|
||||
},
|
||||
extract: {
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
notes() {
|
||||
return this.extract ? this.extract(this.items) : this.items;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.ecsvsegy
|
||||
margin 0 auto
|
||||
|
||||
> * > .post
|
||||
margin-bottom 16px
|
||||
|
||||
> .more
|
||||
margin 32px 16px 16px 16px
|
||||
text-align center
|
||||
|
||||
</style>
|
@ -6,11 +6,9 @@ import note from './note.vue';
|
||||
import notes from './notes.vue';
|
||||
import subNoteContent from './sub-note-content.vue';
|
||||
import window from './window.vue';
|
||||
import noteFormWindow from './post-form-window.vue';
|
||||
import renoteFormWindow from './renote-form-window.vue';
|
||||
import mediaVideo from './media-video.vue';
|
||||
import notifications from './notifications.vue';
|
||||
import noteForm from './post-form.vue';
|
||||
import renoteForm from './renote-form.vue';
|
||||
import notePreview from './note-preview.vue';
|
||||
import noteDetail from './note-detail.vue';
|
||||
@ -25,11 +23,9 @@ Vue.component('mk-note', note);
|
||||
Vue.component('mk-notes', notes);
|
||||
Vue.component('mk-sub-note-content', subNoteContent);
|
||||
Vue.component('mk-window', window);
|
||||
Vue.component('mk-post-form-window', noteFormWindow);
|
||||
Vue.component('mk-renote-form-window', renoteFormWindow);
|
||||
Vue.component('mk-media-video', mediaVideo);
|
||||
Vue.component('mk-notifications', notifications);
|
||||
Vue.component('mk-post-form', noteForm);
|
||||
Vue.component('mk-renote-form', renoteForm);
|
||||
Vue.component('mk-note-preview', notePreview);
|
||||
Vue.component('mk-note-detail', noteDetail);
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
|
||||
|
||||
<div class="empty" v-if="notes.length == 0 && !fetching && inited">{{ $t('@.no-notes') }}</div>
|
||||
<div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div>
|
||||
|
||||
<mk-error v-if="!fetching && !inited" @retry="init()"/>
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
|
||||
<div class="placeholder" v-if="fetching">
|
||||
<template v-for="i in 10">
|
||||
@ -17,8 +17,8 @@
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes">
|
||||
<template v-for="(note, i) in _notes">
|
||||
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :compact="true" ref="note"/>
|
||||
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||
<mk-note :note="note" :key="note.id" :compact="true" ref="note"/>
|
||||
<p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date">
|
||||
<span><fa icon="angle-up"/>{{ note._datetext }}</span>
|
||||
<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
@ -39,153 +39,66 @@ import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import * as config from '../../../config';
|
||||
import shouldMuteNote from '../../../common/scripts/should-mute-note';
|
||||
|
||||
const displayLimit = 30;
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
props: {
|
||||
makePromise: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
paging({
|
||||
captureWindowScroll: true,
|
||||
|
||||
data() {
|
||||
return {
|
||||
notes: [],
|
||||
queue: [],
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
inited: false,
|
||||
more: false
|
||||
};
|
||||
onQueueChanged: (self, x) => {
|
||||
if (x.length > 0) {
|
||||
self.$store.commit('indicate', true);
|
||||
} else {
|
||||
self.$store.commit('indicate', false);
|
||||
}
|
||||
},
|
||||
|
||||
onPrepend: (self, note, silent) => {
|
||||
// 弾く
|
||||
if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false;
|
||||
|
||||
// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
|
||||
if (document.hidden || !self.isScrollTop()) {
|
||||
self.$store.commit('pushBehindNote', note);
|
||||
}
|
||||
|
||||
if (self.isScrollTop()) {
|
||||
// サウンドを再生する
|
||||
if (self.$store.state.device.enableSounds && !silent) {
|
||||
const sound = new Audio(`${config.url}/assets/post.mp3`);
|
||||
sound.volume = self.$store.state.device.soundVolume;
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
||||
props: {
|
||||
pagination: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
_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 = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
|
||||
return note;
|
||||
return (this.items as any).map(item => {
|
||||
const date = new Date(item.createdAt).getDate();
|
||||
const month = new Date(item.createdAt).getMonth() + 1;
|
||||
item._date = date;
|
||||
item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
|
||||
return item;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
},
|
||||
|
||||
methods: {
|
||||
isScrollTop() {
|
||||
return window.scrollY <= 8;
|
||||
},
|
||||
|
||||
focus() {
|
||||
(this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
|
||||
},
|
||||
|
||||
onNoteUpdated(i, note) {
|
||||
Vue.set((this as any).notes, i, note);
|
||||
},
|
||||
|
||||
reload() {
|
||||
this.queue = [];
|
||||
this.notes = [];
|
||||
this.init();
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
this.notes = x.notes;
|
||||
this.more = x.more;
|
||||
}
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
this.$emit('inited');
|
||||
}, e => {
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
}, e => {
|
||||
this.moreFetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
prepend(note, silent = false) {
|
||||
// 弾く
|
||||
if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return;
|
||||
|
||||
// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
|
||||
if (document.hidden || !this.isScrollTop()) {
|
||||
this.$store.commit('pushBehindNote', note);
|
||||
}
|
||||
|
||||
if (this.isScrollTop()) {
|
||||
// Prepend the note
|
||||
this.notes.unshift(note);
|
||||
|
||||
// サウンドを再生する
|
||||
if (this.$store.state.device.enableSounds && !silent) {
|
||||
const sound = new Audio(`${config.url}/assets/post.mp3`);
|
||||
sound.volume = this.$store.state.device.soundVolume;
|
||||
sound.play();
|
||||
}
|
||||
|
||||
// オーバーフローしたら古い投稿は捨てる
|
||||
if (this.notes.length >= displayLimit) {
|
||||
this.notes = this.notes.slice(0, displayLimit);
|
||||
this.more = true;
|
||||
}
|
||||
} else {
|
||||
this.queue.push(note);
|
||||
}
|
||||
},
|
||||
|
||||
append(note) {
|
||||
this.notes.push(note);
|
||||
this.cursor = this.notes[this.notes.length - 1].id
|
||||
},
|
||||
|
||||
releaseQueue() {
|
||||
for (const n of this.queue) {
|
||||
this.prepend(n, true);
|
||||
}
|
||||
this.queue = [];
|
||||
},
|
||||
|
||||
onScroll() {
|
||||
if (this.isScrollTop()) {
|
||||
this.releaseQueue();
|
||||
}
|
||||
|
||||
if (this.$store.state.settings.fetchOnScroll) {
|
||||
const current = window.scrollY + window.innerHeight;
|
||||
if (current > document.body.offsetHeight - 8) this.fetchMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -6,7 +6,7 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="notifications" v-if="notifications.length != 0">
|
||||
<div class="notifications" v-if="!empty">
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
|
||||
<template v-for="(notification, i) in _notifications">
|
||||
@ -125,17 +125,18 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
|
||||
<p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
|
||||
<span><fa icon="angle-up"/>{{ notification._datetext }}</span>
|
||||
<span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
|
||||
<template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }}
|
||||
<button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching">
|
||||
<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
|
||||
</button>
|
||||
<p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p>
|
||||
<p class="empty" v-if="empty">{{ $t('empty') }}</p>
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -143,23 +144,29 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import getNoteSummary from '../../../../../misc/get-note-summary';
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
mixins: [
|
||||
paging({}),
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
fetchingMoreNotifications: false,
|
||||
notifications: [],
|
||||
moreNotifications: false,
|
||||
connection: null,
|
||||
getNoteSummary
|
||||
getNoteSummary,
|
||||
pagination: {
|
||||
endpoint: 'i/notifications',
|
||||
limit: 10,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
_notifications(): any[] {
|
||||
return (this.notifications as any).map(notification => {
|
||||
return (this.items as any).map(notification => {
|
||||
const date = new Date(notification.createdAt).getDate();
|
||||
const month = new Date(notification.createdAt).getMonth() + 1;
|
||||
notification._date = date;
|
||||
@ -171,22 +178,7 @@ export default Vue.extend({
|
||||
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
|
||||
this.connection.on('notification', this.onNotification);
|
||||
|
||||
const max = 10;
|
||||
|
||||
this.$root.api('i/notifications', {
|
||||
limit: max + 1
|
||||
}).then(notifications => {
|
||||
if (notifications.length == max + 1) {
|
||||
this.moreNotifications = true;
|
||||
notifications.pop();
|
||||
}
|
||||
|
||||
this.notifications = notifications;
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
@ -194,33 +186,13 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchMoreNotifications() {
|
||||
this.fetchingMoreNotifications = true;
|
||||
|
||||
const max = 30;
|
||||
|
||||
this.$root.api('i/notifications', {
|
||||
limit: max + 1,
|
||||
untilId: this.notifications[this.notifications.length - 1].id
|
||||
}).then(notifications => {
|
||||
if (notifications.length == max + 1) {
|
||||
this.moreNotifications = true;
|
||||
notifications.pop();
|
||||
} else {
|
||||
this.moreNotifications = false;
|
||||
}
|
||||
this.notifications = this.notifications.concat(notifications);
|
||||
this.fetchingMoreNotifications = false;
|
||||
});
|
||||
},
|
||||
|
||||
onNotification(notification) {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.$root.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
this.notifications.unshift(notification);
|
||||
this.prepend(notification);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
<div class="mk-post-form-window--body">
|
||||
<mk-note-preview v-if="reply" class="notePreview" :note="reply"/>
|
||||
<mk-post-form ref="form"
|
||||
<x-post-form ref="form"
|
||||
:reply="reply"
|
||||
:mention="mention"
|
||||
@posted="onPosted"
|
||||
@ -27,9 +27,15 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import XPostForm from './post-form.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/post-form-window.vue'),
|
||||
|
||||
components: {
|
||||
XPostForm
|
||||
},
|
||||
|
||||
props: {
|
||||
reply: {
|
||||
type: Object,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mk-post-form"
|
||||
<div class="gjisdzwh"
|
||||
@dragover.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@ -28,7 +28,7 @@
|
||||
<fa :icon="['far', 'laugh']"/>
|
||||
</button>
|
||||
<x-post-form-attaches class="files" :class="{ with: poll }" :files="files"/>
|
||||
<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
|
||||
<x-poll-editor class="poll-editor" v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
|
||||
</div>
|
||||
</div>
|
||||
<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
|
||||
@ -72,7 +72,8 @@ export default Vue.extend({
|
||||
|
||||
components: {
|
||||
MkVisibilityChooser,
|
||||
XPostFormAttaches
|
||||
XPostFormAttaches,
|
||||
XPollEditor: () => import('../../../common/views/components/poll-editor.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -518,7 +519,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-post-form
|
||||
.gjisdzwh
|
||||
display block
|
||||
padding 16px
|
||||
background var(--desktopPostFormBg)
|
||||
@ -617,7 +618,7 @@ export default Vue.extend({
|
||||
border-bottom solid 1px var(--primaryAlpha01) !important
|
||||
border-radius 0
|
||||
|
||||
> .mk-poll-editor
|
||||
> .poll-editor
|
||||
background var(--desktopPostFormTextareaBg)
|
||||
border solid 1px var(--primaryAlpha01)
|
||||
border-top none
|
||||
|
@ -10,7 +10,7 @@
|
||||
</footer>
|
||||
</template>
|
||||
<template v-if="quote">
|
||||
<mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/>
|
||||
<x-post-form ref="form" :renote="note" @posted="onChildFormPosted"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -22,6 +22,10 @@ import i18n from '../../../i18n';
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/renote-form.vue'),
|
||||
|
||||
components: {
|
||||
XPostForm: () => import('./post-form.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')">
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
|
||||
<template #header>
|
||||
<slot></slot>
|
||||
</template>
|
||||
@ -11,36 +11,23 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['list'],
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
date: null,
|
||||
makePromise: cursor => this.$root.api('notes/user-list-timeline', {
|
||||
listId: this.list.id,
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'notes/user-list-timeline',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
listId: this.list.id,
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<div class="ecsvsegy" v-if="!fetching">
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<template v-for="favorite in favorites">
|
||||
<mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
<div class="more" v-if="existMore">
|
||||
<ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('.vue'),
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
favorites: [],
|
||||
existMore: false,
|
||||
moreFetching: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
Progress.start();
|
||||
this.fetching = true;
|
||||
|
||||
this.$root.api('i/favorites', {
|
||||
limit: 11
|
||||
}).then(favorites => {
|
||||
if (favorites.length == 11) {
|
||||
this.existMore = true;
|
||||
favorites.pop();
|
||||
}
|
||||
|
||||
this.favorites = favorites;
|
||||
this.fetching = false;
|
||||
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
fetchMore() {
|
||||
this.moreFetching = true;
|
||||
this.$root.api('i/favorites', {
|
||||
limit: 11,
|
||||
untilId: this.favorites[this.favorites.length - 1].id
|
||||
}).then(favorites => {
|
||||
if (favorites.length == 11) {
|
||||
this.existMore = true;
|
||||
favorites.pop();
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
|
||||
this.favorites = this.favorites.concat(favorites);
|
||||
this.moreFetching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.ecsvsegy
|
||||
margin 0 auto
|
||||
|
||||
> * > .post
|
||||
margin-bottom 16px
|
||||
|
||||
> .more
|
||||
margin 32px 16px 16px 16px
|
||||
text-align center
|
||||
|
||||
</style>
|
@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="glowckho" v-if="!fetching">
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<template v-for="note in notes">
|
||||
<mk-note-detail class="post" :note="note" :key="note.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
notes: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
Progress.start();
|
||||
this.fetching = true;
|
||||
|
||||
this.$root.api('notes/featured', {
|
||||
limit: 30
|
||||
}).then(notes => {
|
||||
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
this.notes = notes;
|
||||
this.fetching = false;
|
||||
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.glowckho
|
||||
margin 0 auto
|
||||
|
||||
> * > .post
|
||||
margin-bottom 16px
|
||||
|
||||
> .more
|
||||
margin 32px 16px 16px 16px
|
||||
text-align center
|
||||
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="inited">
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="inited">
|
||||
<template #header>
|
||||
<header class="oxgbmvii">
|
||||
<span><fa icon="search"/> {{ q }}</span>
|
||||
@ -16,30 +16,15 @@ import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/pages/search.vue'),
|
||||
data() {
|
||||
return {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: cursor ? cursor + limit : limit
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'notes/search',
|
||||
limit: 20,
|
||||
params: () => genSearchQuery(this, this.q)
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="inited">
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="inited">
|
||||
<template #header>
|
||||
<header class="wqraeznr">
|
||||
<span><fa icon="hashtag"/> {{ $route.params.tag }}</span>
|
||||
@ -15,30 +15,17 @@ import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/pages/tag.vue'),
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search-by-tag', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
tag: this.$route.params.tag
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: cursor ? cursor + limit : limit
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
pagination: {
|
||||
endpoint: 'notes/search-by-tag',
|
||||
limit: 20,
|
||||
params: {
|
||||
tag: this.$route.params.tag
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')">
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
|
||||
<template #header>
|
||||
<slot></slot>
|
||||
<div v-if="src == 'home' && alone" class="ibpylqas">
|
||||
@ -16,8 +16,6 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/timeline.core.vue'),
|
||||
|
||||
@ -42,7 +40,7 @@ export default Vue.extend({
|
||||
},
|
||||
query: {},
|
||||
endpoint: null,
|
||||
makePromise: null
|
||||
pagination: null
|
||||
};
|
||||
},
|
||||
|
||||
@ -109,25 +107,14 @@ export default Vue.extend({
|
||||
this.connection.on('mention', onNote);
|
||||
}
|
||||
|
||||
this.makePromise = cursor => this.$root.api(this.endpoint, {
|
||||
limit: fetchLimit + 1,
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
untilId: cursor ? cursor : undefined,
|
||||
...this.baseQuery, ...this.query
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
});
|
||||
this.pagination = {
|
||||
endpoint: this.endpoint,
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
...this.baseQuery, ...this.query
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="pwbzawku">
|
||||
<mk-post-form class="form" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
|
||||
<x-post-form class="form" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
|
||||
<div class="main">
|
||||
<component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options">
|
||||
<header class="zahtxcqi">
|
||||
@ -31,8 +31,10 @@ import MkSettingsWindow from '../components/settings-window.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/timeline.vue'),
|
||||
|
||||
components: {
|
||||
XCore
|
||||
XCore,
|
||||
XPostForm: () => import('../components/post-form.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')">
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
|
||||
<template #header>
|
||||
<header class="oh5y2r7l5lx8j6jj791ykeiwgihheguk">
|
||||
<header class="kugajpep">
|
||||
<span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> {{ $t('default') }}</span>
|
||||
<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span>
|
||||
<span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span>
|
||||
@ -17,8 +17,6 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/pages/user/user.timeline.vue'),
|
||||
|
||||
@ -30,28 +28,17 @@ export default Vue.extend({
|
||||
mode: 'default',
|
||||
unreadCount: 0,
|
||||
date: null,
|
||||
makePromise: cursor => this.$root.api('users/notes', {
|
||||
userId: this.user.id,
|
||||
limit: fetchLimit + 1,
|
||||
includeReplies: this.mode == 'with-replies',
|
||||
includeMyRenotes: this.mode != 'my-posts',
|
||||
withFiles: this.mode == 'with-media',
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
untilId: cursor ? cursor : undefined
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
userId: this.user.id,
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
includeReplies: this.mode == 'with-replies',
|
||||
includeMyRenotes: this.mode != 'my-posts',
|
||||
withFiles: this.mode == 'with-media',
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@ -88,7 +75,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.oh5y2r7l5lx8j6jj791ykeiwgihheguk
|
||||
.kugajpep
|
||||
padding 0 8px
|
||||
z-index 10
|
||||
background var(--faceHeader)
|
||||
|
@ -456,6 +456,18 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS)
|
||||
document.body.appendChild(x.$el);
|
||||
return x;
|
||||
},
|
||||
newAsync(vm, props) {
|
||||
return new Promise((res) => {
|
||||
vm().then(vm => {
|
||||
const x = new vm({
|
||||
parent: this,
|
||||
propsData: props
|
||||
}).$mount();
|
||||
document.body.appendChild(x.$el);
|
||||
res(x);
|
||||
});
|
||||
});
|
||||
},
|
||||
dialog(opts) {
|
||||
const vm = this.new(Dialog, opts);
|
||||
const p: any = new Promise((res) => {
|
||||
|
@ -11,16 +11,13 @@ import './style.styl';
|
||||
import init from '../init';
|
||||
|
||||
import MkIndex from './views/pages/index.vue';
|
||||
import MkDeck from '../common/views/deck/deck.vue';
|
||||
import MkSignup from './views/pages/signup.vue';
|
||||
import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||
import MkDrive from './views/pages/drive.vue';
|
||||
import MkWidgets from './views/pages/widgets.vue';
|
||||
import MkMessaging from './views/pages/messaging.vue';
|
||||
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
||||
import MkNote from './views/pages/note.vue';
|
||||
import MkSearch from './views/pages/search.vue';
|
||||
import MkFavorites from './views/pages/favorites.vue';
|
||||
import UI from './views/pages/ui.vue';
|
||||
import MkReversi from './views/pages/games/reversi.vue';
|
||||
import MkTag from './views/pages/tag.vue';
|
||||
@ -29,7 +26,6 @@ import MkFollow from '../common/views/pages/follow.vue';
|
||||
import MkNotFound from '../common/views/pages/not-found.vue';
|
||||
import DeckColumn from '../common/views/deck/deck.column-template.vue';
|
||||
|
||||
import PostForm from './views/components/post-form-dialog.vue';
|
||||
import FileChooser from './views/components/drive-file-chooser.vue';
|
||||
import FolderChooser from './views/components/drive-folder-chooser.vue';
|
||||
|
||||
@ -54,16 +50,16 @@ init((launch, os) => {
|
||||
document.documentElement.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
const vm = this.$root.new(PostForm, {
|
||||
this.$root.newAsync(() => import('./views/components/post-form-dialog.vue').then(m => m.default), {
|
||||
reply: o.reply,
|
||||
mention: o.mention,
|
||||
renote: o.renote
|
||||
}).then(vm => {
|
||||
vm.$once('cancel', recover);
|
||||
vm.$once('posted', recover);
|
||||
if (o.cb) vm.$once('closed', o.cb);
|
||||
(vm as any).focus();
|
||||
});
|
||||
|
||||
vm.$once('cancel', recover);
|
||||
vm.$once('posted', recover);
|
||||
if (o.cb) vm.$once('closed', o.cb);
|
||||
(vm as any).focus();
|
||||
},
|
||||
|
||||
$chooseDriveFile(opts) {
|
||||
@ -114,7 +110,7 @@ init((launch, os) => {
|
||||
mode: 'history',
|
||||
routes: [
|
||||
...(os.store.state.device.inDeckMode
|
||||
? [{ path: '/', name: 'index', component: MkDeck, children: [
|
||||
? [{ path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [
|
||||
{ path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [
|
||||
{ path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) },
|
||||
{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
|
||||
@ -123,10 +119,10 @@ init((launch, os) => {
|
||||
{ path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) },
|
||||
{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) },
|
||||
{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) },
|
||||
{ path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) },
|
||||
{ path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) },
|
||||
{ path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
|
||||
{ path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
|
||||
{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) },
|
||||
{ path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) },
|
||||
{ path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
|
||||
{ path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
|
||||
{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) },
|
||||
@ -138,14 +134,14 @@ init((launch, os) => {
|
||||
]),
|
||||
{ path: '/signup', name: 'signup', component: MkSignup },
|
||||
{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) },
|
||||
{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
|
||||
{ path: '/i/favorites', name: 'favorites', component: UI, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'mobile' }) },
|
||||
{ path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
|
||||
{ path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
|
||||
{ path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) },
|
||||
{ path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
|
||||
{ path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) },
|
||||
{ path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
|
||||
{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
|
||||
{ path: '/i/widgets', name: 'widgets', component: () => import('./views/pages/widgets.vue').then(m => m.default) },
|
||||
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
|
||||
{ path: '/i/messaging/group/:group', component: MkMessagingRoom },
|
||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||
@ -157,7 +153,7 @@ init((launch, os) => {
|
||||
{ path: '/selectdrive', component: MkSelectDrive },
|
||||
{ path: '/search', component: MkSearch },
|
||||
{ path: '/tags/:tag', component: MkTag },
|
||||
{ path: '/featured', name: 'featured', component: () => import('./views/pages/featured.vue').then(m => m.default) },
|
||||
{ path: '/featured', name: 'featured', component: UI, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'mobile' }) },
|
||||
{ path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
|
||||
{ path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
|
||||
{ path: '/share', component: MkShare },
|
||||
|
52
src/client/app/mobile/views/components/detail-notes.vue
Normal file
52
src/client/app/mobile/views/components/detail-notes.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="fdcvngpy">
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<template v-for="note in notes">
|
||||
<mk-note-detail class="post" :note="note" :key="note.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="more" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
mixins: [
|
||||
paging({
|
||||
captureWindowScroll: true,
|
||||
}),
|
||||
],
|
||||
|
||||
props: {
|
||||
pagination: {
|
||||
required: true
|
||||
},
|
||||
extract: {
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
notes() {
|
||||
return this.extract ? this.extract(this.items) : this.items;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.fdcvngpy
|
||||
> * > .post
|
||||
margin-bottom 8px
|
||||
|
||||
@media (min-width 500px)
|
||||
> * > .post
|
||||
margin-bottom 16px
|
||||
|
||||
</style>
|
@ -14,7 +14,6 @@ import notificationPreview from './notification-preview.vue';
|
||||
import userTimeline from './user-timeline.vue';
|
||||
import userListTimeline from './user-list-timeline.vue';
|
||||
import uiContainer from './ui-container.vue';
|
||||
import postForm from './post-form.vue';
|
||||
|
||||
Vue.component('mk-ui', ui);
|
||||
Vue.component('mk-note', note);
|
||||
@ -30,4 +29,3 @@ Vue.component('mk-notification-preview', notificationPreview);
|
||||
Vue.component('mk-user-timeline', userTimeline);
|
||||
Vue.component('mk-user-list-timeline', userListTimeline);
|
||||
Vue.component('ui-container', uiContainer);
|
||||
Vue.component('mk-post-form', postForm);
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="ivaojijs" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
|
||||
<div class="empty" v-if="notes.length == 0 && !fetching && inited">{{ $t('@.no-notes') }}</div>
|
||||
<div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div>
|
||||
|
||||
<mk-error v-if="!fetching && !inited" @retry="init()"/>
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
|
||||
<div class="placeholder" v-if="fetching">
|
||||
<template v-for="i in 10">
|
||||
@ -13,8 +13,8 @@
|
||||
<!-- トランジションを有効にするとなぜかメモリリークする -->
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
|
||||
<template v-for="(note, i) in _notes">
|
||||
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
||||
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||
<mk-note :note="note" :key="note.id"/>
|
||||
<p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date">
|
||||
<span><fa icon="angle-up"/>{{ note._datetext }}</span>
|
||||
<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
@ -34,158 +34,56 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import shouldMuteNote from '../../../common/scripts/should-mute-note';
|
||||
|
||||
const displayLimit = 30;
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
|
||||
props: {
|
||||
makePromise: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
paging({
|
||||
captureWindowScroll: true,
|
||||
|
||||
data() {
|
||||
return {
|
||||
notes: [],
|
||||
queue: [],
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
inited: false,
|
||||
more: false
|
||||
};
|
||||
onQueueChanged: (self, x) => {
|
||||
if (x.length > 0) {
|
||||
self.$store.commit('indicate', true);
|
||||
} else {
|
||||
self.$store.commit('indicate', false);
|
||||
}
|
||||
},
|
||||
|
||||
onPrepend: (self, note) => {
|
||||
// 弾く
|
||||
if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false;
|
||||
|
||||
// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
|
||||
if (document.hidden || !self.isScrollTop()) {
|
||||
self.$store.commit('pushBehindNote', note);
|
||||
}
|
||||
},
|
||||
|
||||
onInited: (self) => {
|
||||
self.$emit('loaded');
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
||||
props: {
|
||||
pagination: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
_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 = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
|
||||
return note;
|
||||
return (this.items as any).map(item => {
|
||||
const date = new Date(item.createdAt).getDate();
|
||||
const month = new Date(item.createdAt).getMonth() + 1;
|
||||
item._date = date;
|
||||
item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
|
||||
return item;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
queue(x) {
|
||||
if (x.length > 0) {
|
||||
this.$store.commit('indicate', true);
|
||||
} else {
|
||||
this.$store.commit('indicate', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
},
|
||||
|
||||
methods: {
|
||||
isScrollTop() {
|
||||
return window.scrollY <= 8;
|
||||
},
|
||||
|
||||
onNoteUpdated(i, note) {
|
||||
Vue.set((this as any).notes, i, note);
|
||||
},
|
||||
|
||||
reload() {
|
||||
this.queue = [];
|
||||
this.notes = [];
|
||||
this.init();
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
this.notes = x.notes;
|
||||
this.more = x.more;
|
||||
}
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
this.$emit('inited');
|
||||
}, e => {
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
}, e => {
|
||||
this.moreFetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
prepend(note, silent = false) {
|
||||
// 弾く
|
||||
if (shouldMuteNote(this.$store.state.i, this.$store.state.settings, note)) return;
|
||||
|
||||
// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
|
||||
if (document.hidden || !this.isScrollTop()) {
|
||||
this.$store.commit('pushBehindNote', note);
|
||||
}
|
||||
|
||||
if (this.isScrollTop()) {
|
||||
// Prepend the note
|
||||
this.notes.unshift(note);
|
||||
|
||||
// オーバーフローしたら古い投稿は捨てる
|
||||
if (this.notes.length >= displayLimit) {
|
||||
this.notes = this.notes.slice(0, displayLimit);
|
||||
this.more = true;
|
||||
}
|
||||
} else {
|
||||
this.queue.push(note);
|
||||
}
|
||||
},
|
||||
|
||||
append(note) {
|
||||
this.notes.push(note);
|
||||
this.cursor = this.notes[this.notes.length - 1].id
|
||||
},
|
||||
|
||||
releaseQueue() {
|
||||
for (const n of this.queue) {
|
||||
this.prepend(n, true);
|
||||
}
|
||||
this.queue = [];
|
||||
},
|
||||
|
||||
onScroll() {
|
||||
if (this.isScrollTop()) {
|
||||
this.releaseQueue();
|
||||
}
|
||||
|
||||
if (this.$store.state.settings.fetchOnScroll) {
|
||||
// 親要素が display none だったら弾く
|
||||
// https://github.com/syuilo/misskey/issues/1569
|
||||
// http://d.hatena.ne.jp/favril/20091105/1257403319
|
||||
if (this.$el.offsetHeight == 0) return;
|
||||
|
||||
const current = window.scrollY + window.innerHeight;
|
||||
if (current > document.body.offsetHeight - 8) this.fetchMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -71,15 +71,15 @@
|
||||
</div>
|
||||
|
||||
<template v-if="notification.type == 'quote'">
|
||||
<mk-note :note="notification.note" @update:note="onNoteUpdated"/>
|
||||
<mk-note :note="notification.note"/>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type == 'reply'">
|
||||
<mk-note :note="notification.note" @update:note="onNoteUpdated"/>
|
||||
<mk-note :note="notification.note"/>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type == 'mention'">
|
||||
<mk-note :note="notification.note" @update:note="onNoteUpdated"/>
|
||||
<mk-note :note="notification.note"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -95,17 +95,6 @@ export default Vue.extend({
|
||||
getNoteSummary
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onNoteUpdated(note) {
|
||||
switch (this.notification.type) {
|
||||
case 'quote':
|
||||
case 'reply':
|
||||
case 'mention':
|
||||
Vue.set(this.notification, 'note', note);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -10,41 +10,49 @@
|
||||
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div">
|
||||
<template v-for="(notification, i) in _notifications">
|
||||
<mk-notification :notification="notification" :key="notification.id"/>
|
||||
<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
|
||||
<p class="date" :key="notification.id + '_date'" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date">
|
||||
<span><fa icon="angle-up"/>{{ notification._datetext }}</span>
|
||||
<span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
|
||||
<template v-if="fetchingMoreNotifications"><fa icon="spinner" pulse fixed-width/></template>
|
||||
{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }}
|
||||
<button class="more" v-if="more" @click="fetchMore" :disabled="moreFetching">
|
||||
<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
|
||||
{{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
|
||||
</button>
|
||||
|
||||
<p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p>
|
||||
<p class="empty" v-if="empty">{{ $t('empty') }}</p>
|
||||
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import paging from '../../../common/scripts/paging';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/components/notifications.vue'),
|
||||
|
||||
mixins: [
|
||||
paging({}),
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
fetchingMoreNotifications: false,
|
||||
notifications: [],
|
||||
moreNotifications: false,
|
||||
connection: null
|
||||
connection: null,
|
||||
pagination: {
|
||||
endpoint: 'i/notifications',
|
||||
limit: 15,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
_notifications(): any[] {
|
||||
return (this.notifications as any).map(notification => {
|
||||
return (this.items as any).map(notification => {
|
||||
const date = new Date(notification.createdAt).getDate();
|
||||
const month = new Date(notification.createdAt).getMonth() + 1;
|
||||
notification._date = date;
|
||||
@ -55,76 +63,23 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
|
||||
this.connection.on('notification', this.onNotification);
|
||||
|
||||
const max = 15;
|
||||
|
||||
this.$root.api('i/notifications', {
|
||||
limit: max + 1
|
||||
}).then(notifications => {
|
||||
if (notifications.length == max + 1) {
|
||||
this.moreNotifications = true;
|
||||
notifications.pop();
|
||||
}
|
||||
|
||||
this.notifications = notifications;
|
||||
this.fetching = false;
|
||||
this.$emit('fetched');
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchMoreNotifications() {
|
||||
if (this.fetchingMoreNotifications) return;
|
||||
|
||||
this.fetchingMoreNotifications = true;
|
||||
|
||||
const max = 30;
|
||||
|
||||
this.$root.api('i/notifications', {
|
||||
limit: max + 1,
|
||||
untilId: this.notifications[this.notifications.length - 1].id
|
||||
}).then(notifications => {
|
||||
if (notifications.length == max + 1) {
|
||||
this.moreNotifications = true;
|
||||
notifications.pop();
|
||||
} else {
|
||||
this.moreNotifications = false;
|
||||
}
|
||||
this.notifications = this.notifications.concat(notifications);
|
||||
this.fetchingMoreNotifications = false;
|
||||
});
|
||||
},
|
||||
|
||||
onNotification(notification) {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.$root.stream.send('readNotification', {
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
this.notifications.unshift(notification);
|
||||
this.prepend(notification);
|
||||
},
|
||||
|
||||
onScroll() {
|
||||
if (this.$store.state.settings.fetchOnScroll) {
|
||||
// 親要素が display none だったら弾く
|
||||
// https://github.com/syuilo/misskey/issues/1569
|
||||
// http://d.hatena.ne.jp/favril/20091105/1257403319
|
||||
if (this.$el.offsetHeight == 0) return;
|
||||
|
||||
const current = window.scrollY + window.innerHeight;
|
||||
if (current > document.body.offsetHeight - 8) this.fetchMoreNotifications();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
|
||||
<div class="bg" ref="bg"></div>
|
||||
<div class="main" ref="main">
|
||||
<mk-post-form ref="form"
|
||||
<x-post-form ref="form"
|
||||
:reply="reply"
|
||||
:renote="renote"
|
||||
:mention="mention"
|
||||
@ -17,8 +17,13 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import anime from 'animejs';
|
||||
import XPostForm from './post-form.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPostForm
|
||||
},
|
||||
|
||||
props: {
|
||||
reply: {
|
||||
type: Object,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mk-post-form">
|
||||
<div class="gafaadew">
|
||||
<div class="form">
|
||||
<header>
|
||||
<button class="cancel" @click="cancel"><fa icon="times"/></button>
|
||||
@ -22,7 +22,7 @@
|
||||
<input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('annotations')" v-autocomplete="{ model: 'cw' }">
|
||||
<textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }"></textarea>
|
||||
<x-post-form-attaches class="attaches" :files="files"/>
|
||||
<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
|
||||
<x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
|
||||
<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
|
||||
<footer>
|
||||
<button class="upload" @click="chooseFile"><fa icon="upload"/></button>
|
||||
@ -64,7 +64,8 @@ import XPostFormAttaches from '../../../common/views/components/post-form-attach
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/components/post-form.vue'),
|
||||
components: {
|
||||
XPostFormAttaches
|
||||
XPostFormAttaches,
|
||||
XPollEditor: () => import('../../../common/views/components/poll-editor.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -386,7 +387,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-post-form
|
||||
.gafaadew
|
||||
max-width 500px
|
||||
width calc(100% - 16px)
|
||||
margin 8px auto
|
||||
|
@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="header" ref="root" :class="{ shadow: $store.state.device.useShadow }">
|
||||
<p class="warn" v-if="env != 'production'">{{ $t('@.do-not-use-in-production') }} <a href="/assets/flush.html?force">Flush</a></p>
|
||||
<div class="main" ref="main">
|
||||
<div class="backdrop"></div>
|
||||
<div class="content" ref="mainContainer">
|
||||
|
@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
</div>
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['list'],
|
||||
|
||||
@ -16,28 +12,17 @@ export default Vue.extend({
|
||||
return {
|
||||
connection: null,
|
||||
date: null,
|
||||
makePromise: cursor => this.$root.api('notes/user-list-timeline', {
|
||||
listId: this.list.id,
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'notes/user-list-timeline',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
listId: this.list.id,
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -1,15 +1,11 @@
|
||||
<template>
|
||||
<div class="mk-user-timeline">
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
</div>
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/components/user-timeline.vue'),
|
||||
|
||||
@ -18,26 +14,15 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
date: null,
|
||||
makePromise: cursor => this.$root.api('users/notes', {
|
||||
userId: this.user.id,
|
||||
limit: fetchLimit + 1,
|
||||
withFiles: this.withMedia,
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
untilId: cursor ? cursor : undefined
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
userId: this.user.id,
|
||||
withFiles: this.withMedia,
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<template #header><span style="margin-right:4px;"><fa icon="star"/></span>{{ $t('@.favorites') }}</template>
|
||||
|
||||
<main>
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<template v-for="favorite in favorites">
|
||||
<mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
favorites: [],
|
||||
existMore: false,
|
||||
moreFetching: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
mounted() {
|
||||
document.title = `${this.$root.instanceName} | ${this.$t('@.favorites')}`;
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
Progress.start();
|
||||
this.fetching = true;
|
||||
|
||||
this.$root.api('i/favorites', {
|
||||
limit: 11
|
||||
}).then(favorites => {
|
||||
if (favorites.length == 11) {
|
||||
this.existMore = true;
|
||||
favorites.pop();
|
||||
}
|
||||
|
||||
this.favorites = favorites;
|
||||
this.fetching = false;
|
||||
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
fetchMore() {
|
||||
this.moreFetching = true;
|
||||
this.$root.api('i/favorites', {
|
||||
limit: 11,
|
||||
untilId: this.favorites[this.favorites.length - 1].id
|
||||
}).then(favorites => {
|
||||
if (favorites.length == 11) {
|
||||
this.existMore = true;
|
||||
favorites.pop();
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
|
||||
this.favorites = this.favorites.concat(favorites);
|
||||
this.moreFetching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
> * > .post
|
||||
margin-bottom 8px
|
||||
|
||||
@media (min-width 500px)
|
||||
> * > .post
|
||||
margin-bottom 16px
|
||||
|
||||
</style>
|
@ -1,61 +0,0 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<template #header><span style="margin-right:4px;"><fa :icon="faNewspaper"/></span>{{ $t('@.featured-notes') }}</template>
|
||||
|
||||
<main>
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<template v-for="note in notes">
|
||||
<mk-note-detail class="post" :note="note" :key="note.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { faNewspaper } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(''),
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
notes: [],
|
||||
faNewspaper
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
Progress.start();
|
||||
this.fetching = true;
|
||||
|
||||
this.$root.api('notes/featured', {
|
||||
limit: 30
|
||||
}).then(notes => {
|
||||
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
this.notes = notes;
|
||||
this.fetching = false;
|
||||
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
> * > .post
|
||||
margin-bottom 8px
|
||||
|
||||
@media (min-width 500px)
|
||||
> * > .post
|
||||
margin-bottom 16px
|
||||
|
||||
</style>
|
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
</ui-container>
|
||||
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="() => $emit('loaded')"/>
|
||||
<mk-notes ref="timeline" :pagination="pagination" @loaded="() => $emit('loaded')"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -15,8 +15,6 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
const fetchLimit = 10;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/pages/home.timeline.vue'),
|
||||
|
||||
@ -43,7 +41,7 @@ export default Vue.extend({
|
||||
},
|
||||
query: {},
|
||||
endpoint: null,
|
||||
makePromise: null
|
||||
pagination: null
|
||||
};
|
||||
},
|
||||
|
||||
@ -110,25 +108,14 @@ export default Vue.extend({
|
||||
this.connection.on('mention', onNote);
|
||||
}
|
||||
|
||||
this.makePromise = cursor => this.$root.api(this.endpoint, {
|
||||
limit: fetchLimit + 1,
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
untilId: cursor ? cursor : undefined,
|
||||
...this.baseQuery, ...this.query
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
});
|
||||
this.pagination = {
|
||||
endpoint: this.endpoint,
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
...this.baseQuery, ...this.query
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -3,7 +3,7 @@
|
||||
<template #header><fa icon="search"/> {{ q }}</template>
|
||||
|
||||
<main>
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"/>
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="inited"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
@ -14,42 +14,27 @@ import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/pages/search.vue'),
|
||||
data() {
|
||||
return {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
pagination: {
|
||||
endpoint: 'notes/search',
|
||||
limit: 20,
|
||||
params: () => genSearchQuery(this, this.q)
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.$refs.timeline.reload();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
q(): string {
|
||||
return this.$route.query.q;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.$refs.timeline.reload();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.title = `${this.$t('search')}: ${this.q} | ${this.$root.instanceName}`;
|
||||
},
|
||||
|
@ -3,7 +3,7 @@
|
||||
<template #header><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</template>
|
||||
|
||||
<main>
|
||||
<mk-notes ref="timeline" :make-promise="makePromise" @inited="inited"/>
|
||||
<mk-notes ref="timeline" :pagination="pagination" @inited="inited"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
@ -13,30 +13,17 @@ import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/pages/tag.vue'),
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search-by-tag', {
|
||||
limit: limit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
tag: this.$route.params.tag
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
more: false
|
||||
};
|
||||
pagination: {
|
||||
endpoint: 'notes/search-by-tag',
|
||||
limit: 20,
|
||||
params: {
|
||||
tag: this.$route.params.tag
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -19,8 +19,6 @@
|
||||
<x-activity :user="user"/>
|
||||
</div>
|
||||
</ui-container>
|
||||
<mk-user-list :make-promise="makeFrequentlyRepliedUsersPromise" :icon-only="true"><fa icon="users"/> {{ $t('frequently-replied-users') }}</mk-user-list>
|
||||
<mk-user-list v-if="$store.getters.isSignedIn && $store.state.i.id !== user.id" :make-promise="makeFollowersYouKnowPromise" :icon-only="true"><fa icon="users"/> {{ $t('followers-you-know') }}</mk-user-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
Reference in New Issue
Block a user