diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue index fc3a7af75d..53d08a0eca 100644 --- a/src/client/app/desktop/views/components/mentions.vue +++ b/src/client/app/desktop/views/components/mentions.vue @@ -1,8 +1,8 @@ <template> <div class="mk-mentions"> <header> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて</span> - <span :data-is-active="mode == 'following'" @click="mode = 'following'">フォロー中</span> + <span :data-active="mode == 'all'" @click="mode = 'all'">すべて</span> + <span :data-active="mode == 'following'" @click="mode = 'following'">フォロー中</span> </header> <div class="fetching" v-if="fetching"> <mk-ellipsis-icon/> @@ -98,7 +98,7 @@ export default Vue.extend({ font-size 18px color #555 - &:not([data-is-active]) + &:not([data-active]) color $theme-color cursor pointer diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index 01e1f5c2f0..fa7a782b7b 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -31,6 +31,7 @@ <script lang="ts"> import Vue from 'vue'; import { url } from '../../../config'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; import XNote from './notes.note.vue'; @@ -53,6 +54,7 @@ export default Vue.extend({ requestInitPromise: null as () => Promise<any[]>, notes: [], queue: [], + unreadCount: 0, fetching: true, moreFetching: false }; @@ -71,10 +73,12 @@ export default Vue.extend({ }, mounted() { + document.addEventListener('visibilitychange', this.onVisibilitychange, false); window.addEventListener('scroll', this.onScroll); }, beforeDestroy() { + document.removeEventListener('visibilitychange', this.onVisibilitychange); window.removeEventListener('scroll', this.onScroll); }, @@ -130,6 +134,12 @@ export default Vue.extend({ } //#endregion + // 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 + if ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; + } + if (this.isScrollTop()) { // Prepend the note this.notes.unshift(note); @@ -172,9 +182,21 @@ export default Vue.extend({ this.moreFetching = false; }, + clearNotification() { + this.unreadCount = 0; + document.title = 'Misskey'; + }, + + onVisibilitychange() { + if (!document.hidden) { + this.clearNotification(); + } + }, + onScroll() { if (this.isScrollTop()) { this.releaseQueue(); + this.clearNotification(); } if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index d50b41b846..f5f13cbd56 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -1,10 +1,10 @@ <template> <div class="mk-timeline"> <header> - <span :data-is-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> - <span :data-is-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span> - <span :data-is-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span> - <span :data-is-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> + <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> + <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span> + <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span> + <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> <button @click="chooseList" title="リスト">%fa:list%</button> </header> <x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> @@ -93,7 +93,7 @@ root(isDark) font-size 12px user-select none - &[data-is-active] + &[data-active] color $theme-color cursor default font-weight bold @@ -108,7 +108,7 @@ root(isDark) height 2px background $theme-color - &:not([data-is-active]) + &:not([data-active]) color isDark ? #9aa2a7 : #6f7477 cursor pointer diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue index 8a1814f99c..ee983a969c 100644 --- a/src/client/app/desktop/views/components/user-list-timeline.vue +++ b/src/client/app/desktop/views/components/user-list-timeline.vue @@ -87,7 +87,7 @@ export default Vue.extend({ }, onUserRemoved() { this.fetch(); - }, + } } }); </script> diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue index a08e76f573..e8f4c94d42 100644 --- a/src/client/app/desktop/views/components/users-list.vue +++ b/src/client/app/desktop/views/components/users-list.vue @@ -2,8 +2,8 @@ <div class="mk-users-list"> <nav> <div> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span> - <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span> + <span :data-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span> + <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span> </div> </nav> <div class="users" v-if="!fetching && users.length != 0"> @@ -98,7 +98,7 @@ export default Vue.extend({ * pointer-events none - &[data-is-active] + &[data-active] font-weight bold color $theme-color border-color $theme-color diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue index 754be8c04f..9c9840c190 100644 --- a/src/client/app/desktop/views/pages/user/user.timeline.vue +++ b/src/client/app/desktop/views/pages/user/user.timeline.vue @@ -1,9 +1,9 @@ <template> <div class="timeline"> <header> - <span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span> - <span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span> - <span :data-is-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span> + <span :data-active="mode == 'default'" @click="mode = 'default'">投稿</span> + <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span> + <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span> </header> <div class="loading" v-if="fetching"> <mk-ellipsis-icon/> @@ -114,7 +114,7 @@ export default Vue.extend({ font-size 18px color #555 - &:not([data-is-active]) + &:not([data-active]) color $theme-color cursor pointer diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts index 9a0a52d106..5ed8427b05 100644 --- a/src/client/app/mobile/views/components/index.ts +++ b/src/client/app/mobile/views/components/index.ts @@ -19,6 +19,7 @@ import notificationPreview from './notification-preview.vue'; import usersList from './users-list.vue'; import userPreview from './user-preview.vue'; import userTimeline from './user-timeline.vue'; +import userListTimeline from './user-list-timeline.vue'; import activity from './activity.vue'; import widgetContainer from './widget-container.vue'; @@ -41,5 +42,6 @@ Vue.component('mk-notification-preview', notificationPreview); Vue.component('mk-users-list', usersList); Vue.component('mk-user-preview', userPreview); Vue.component('mk-user-timeline', userTimeline); +Vue.component('mk-user-list-timeline', userListTimeline); Vue.component('mk-activity', activity); Vue.component('mk-widget-container', widgetContainer); diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 137e15c6de..703b51d678 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -36,6 +36,7 @@ <script lang="ts"> import Vue from 'vue'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; const displayLimit = 30; @@ -52,6 +53,7 @@ export default Vue.extend({ requestInitPromise: null as () => Promise<any[]>, notes: [], queue: [], + unreadCount: 0, fetching: true, moreFetching: false }; @@ -70,10 +72,12 @@ export default Vue.extend({ }, mounted() { + document.addEventListener('visibilitychange', this.onVisibilitychange, false); window.addEventListener('scroll', this.onScroll); }, beforeDestroy() { + document.removeEventListener('visibilitychange', this.onVisibilitychange); window.removeEventListener('scroll', this.onScroll); }, @@ -125,6 +129,12 @@ export default Vue.extend({ } //#endregion + // 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知 + if ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; + } + if (this.isScrollTop()) { // Prepend the note this.notes.unshift(note); @@ -160,9 +170,21 @@ export default Vue.extend({ this.moreFetching = false; }, + clearNotification() { + this.unreadCount = 0; + document.title = 'Misskey'; + }, + + onVisibilitychange() { + if (!document.hidden) { + this.clearNotification(); + } + }, + onScroll() { if (this.isScrollTop()) { this.releaseQueue(); + this.clearNotification(); } if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue new file mode 100644 index 0000000000..ee983a969c --- /dev/null +++ b/src/client/app/mobile/views/components/user-list-timeline.vue @@ -0,0 +1,93 @@ +<template> +<div> + <mk-notes ref="timeline" :more="existMore ? more : null"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { UserListStream } from '../../../common/scripts/streaming/user-list'; + +const fetchLimit = 10; + +export default Vue.extend({ + props: ['list'], + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null + }; + }, + watch: { + $route: 'init' + }, + mounted() { + this.init(); + }, + beforeDestroy() { + this.connection.close(); + }, + methods: { + init() { + if (this.connection) this.connection.close(); + this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id); + this.connection.on('note', this.onNote); + this.connection.on('userAdded', this.onUserAdded); + this.connection.on('userRemoved', this.onUserRemoved); + + this.fetch(); + }, + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('notes/user-list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + this.$emit('loaded'); + }, rej); + })); + }, + more() { + this.moreFetching = true; + + (this as any).api('notes/list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + notes.forEach(n => (this.$refs.timeline as any).append(n)); + this.moreFetching = false; + }); + }, + onNote(note) { + // Prepend a note + (this.$refs.timeline as any).prepend(note); + }, + onUserAdded() { + this.fetch(); + }, + onUserRemoved() { + this.fetch(); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue index 8fa7a9cbe6..67a38a8955 100644 --- a/src/client/app/mobile/views/components/users-list.vue +++ b/src/client/app/mobile/views/components/users-list.vue @@ -1,8 +1,8 @@ <template> <div class="mk-users-list"> <nav> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span> - <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span> + <span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span> + <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span> </nav> <div class="users" v-if="!fetching && users.length != 0"> <mk-user-preview v-for="u in users" :user="u" :key="u.id"/> @@ -85,7 +85,7 @@ export default Vue.extend({ color #657786 border-bottom solid 2px transparent - &[data-is-active] + &[data-active] font-weight bold color $theme-color border-color $theme-color diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index 42ed454d22..5f4bd6dcd8 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -13,7 +13,6 @@ <script lang="ts"> import Vue from 'vue'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; const fetchLimit = 10; @@ -73,8 +72,6 @@ export default Vue.extend({ this.connection.on('unfollow', this.onChangeFollowing); } - document.addEventListener('visibilitychange', this.onVisibilitychange, false); - this.fetch(); }, @@ -85,8 +82,6 @@ export default Vue.extend({ this.connection.off('unfollow', this.onChangeFollowing); } this.stream.dispose(this.connectionId); - - document.removeEventListener('visibilitychange', this.onVisibilitychange); }, methods: { @@ -133,11 +128,6 @@ export default Vue.extend({ }, onNote(note) { - if (document.hidden && note.userId !== (this as any).os.i.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; - } - // Prepend a note (this.$refs.timeline as any).prepend(note); }, @@ -153,13 +143,6 @@ export default Vue.extend({ warp(date) { this.date = date; this.fetch(); - }, - - onVisibilitychange() { - if (!document.hidden) { - this.unreadCount = 0; - document.title = 'Misskey'; - } } } }); diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 3b152b3952..92d34fa83b 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -5,7 +5,7 @@ <span v-if="src == 'home'">%fa:home%ホーム</span> <span v-if="src == 'local'">%fa:R comments%ローカル</span> <span v-if="src == 'global'">%fa:globe%グローバル</span> - <span v-if="src == 'list'">%fa:list%{{ list.title }}</span> + <span v-if="src.startsWith('list')">%fa:list%{{ list.title }}</span> </span> <span style="margin-left:8px"> <template v-if="!showNav">%fa:angle-down%</template> @@ -21,9 +21,14 @@ <div class="nav" v-if="showNav"> <div class="bg" @click="showNav = false"></div> <div class="body"> - <span :data-is-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> - <span :data-is-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span> - <span :data-is-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span> + <div> + <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> + <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span> + <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span> + <template v-if="lists"> + <span v-for="l in lists" :data-active="src == 'list:' + l.id" @click="src = 'list:' + l.id; list = l" :key="l.id">%fa:list% {{ l.title }}</span> + </template> + </div> </div> </div> @@ -31,7 +36,7 @@ <x-tl v-if="src == 'home'" ref="tl" key="home" src="home" @loaded="onLoaded"/> <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> - <mk-user-list-timeline v-if="src == 'list'" ref="tl" key="list" :list="list"/> + <mk-user-list-timeline v-if="src.startsWith('list:')" ref="tl" key="list" :list="list"/> </div> </main> </mk-ui> @@ -51,10 +56,25 @@ export default Vue.extend({ return { src: 'home', list: null, + lists: null, showNav: false }; }, + watch: { + src() { + this.showNav = false; + }, + + showNav(v) { + if (v && this.lists === null) { + (this as any).api('users/lists/list').then(lists => { + this.lists = lists; + }); + } + } + }, + mounted() { document.title = 'Misskey'; document.documentElement.style.background = '#313a42'; @@ -79,6 +99,8 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> +@import '~const.styl' + main > .nav > .bg @@ -93,10 +115,52 @@ main > .body position fixed z-index 10001 - top 48px + top 56px left 0 + right 0 + width 300px + margin 0 auto background #fff border-radius 8px + box-shadow 0 0 16px rgba(0, 0, 0, 0.1) + + $balloon-size = 16px + + &:before + content "" + display block + position absolute + top -($balloon-size * 2) + left s('calc(50% - %s)', $balloon-size) + border-top solid $balloon-size transparent + border-left solid $balloon-size transparent + border-right solid $balloon-size transparent + border-bottom solid $balloon-size $border-color + + &:after + content "" + display block + position absolute + top -($balloon-size * 2) + 1.5px + left s('calc(50% - %s)', $balloon-size) + border-top solid $balloon-size transparent + border-left solid $balloon-size transparent + border-right solid $balloon-size transparent + border-bottom solid $balloon-size #fff + + > div + padding 8px 0 + + > * + display block + padding 8px 16px + + &[data-active] + color $theme-color-foreground + background $theme-color + + &:not([data-active]):hover + background #eee > .tl max-width 600px diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index 3ff9057f73..73b8e24315 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -45,9 +45,9 @@ </header> <nav> <div class="nav-container"> - <a :data-is-active="page == 'home'" @click="page = 'home'">%i18n:@overview%</a> - <a :data-is-active="page == 'notes'" @click="page = 'notes'">%i18n:@timeline%</a> - <a :data-is-active="page == 'media'" @click="page = 'media'">%i18n:@media%</a> + <a :data-active="page == 'home'" @click="page = 'home'">%i18n:@overview%</a> + <a :data-active="page == 'notes'" @click="page = 'notes'">%i18n:@timeline%</a> + <a :data-active="page == 'media'" @click="page = 'media'">%i18n:@media%</a> </div> </nav> <div class="body"> @@ -256,7 +256,7 @@ main color #657786 border-bottom solid 2px transparent - &[data-is-active] + &[data-active] font-weight bold color $theme-color border-color $theme-color