diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index f5e0ee118e..a137a57070 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -15,6 +15,7 @@ <script lang="ts"> import Vue from 'vue'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; const fetchLimit = 10; @@ -33,6 +34,7 @@ export default Vue.extend({ existMore: false, connection: null, connectionId: null, + unreadCount: 0, date: null }; }, @@ -74,6 +76,7 @@ export default Vue.extend({ } document.addEventListener('keydown', this.onKeydown); + document.addEventListener('visibilitychange', this.onVisibilitychange, false); this.fetch(); }, @@ -87,10 +90,11 @@ export default Vue.extend({ this.stream.dispose(this.connectionId); document.removeEventListener('keydown', this.onKeydown); + document.removeEventListener('visibilitychange', this.onVisibilitychange); }, methods: { - fetch(cb?) { + fetch() { this.fetching = true; (this.$refs.timeline as any).init(() => new Promise((res, rej) => { @@ -107,7 +111,6 @@ export default Vue.extend({ res(notes); this.fetching = false; this.$emit('loaded'); - if (cb) cb(); }, rej); })); }, @@ -134,6 +137,11 @@ 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); }, @@ -151,13 +159,20 @@ export default Vue.extend({ this.fetch(); }, + onVisibilitychange() { + if (!document.hidden) { + this.unreadCount = 0; + document.title = 'Misskey'; + } + }, + onKeydown(e) { if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { if (e.which == 84) { // t this.focus(); } } - }, + } } }); </script> diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts index 9346700304..9a0a52d106 100644 --- a/src/client/app/mobile/views/components/index.ts +++ b/src/client/app/mobile/views/components/index.ts @@ -1,7 +1,6 @@ import Vue from 'vue'; import ui from './ui.vue'; -import timeline from './timeline.vue'; import note from './note.vue'; import notes from './notes.vue'; import mediaImage from './media-image.vue'; @@ -24,7 +23,6 @@ import activity from './activity.vue'; import widgetContainer from './widget-container.vue'; Vue.component('mk-ui', ui); -Vue.component('mk-timeline', timeline); Vue.component('mk-note', note); Vue.component('mk-notes', notes); Vue.component('mk-media-image', mediaImage); diff --git a/src/client/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue deleted file mode 100644 index 561034917e..0000000000 --- a/src/client/app/mobile/views/components/timeline.vue +++ /dev/null @@ -1,119 +0,0 @@ -<template> -<div class="mk-timeline"> - <mk-friends-maker v-if="alone"/> - - <mk-notes ref="timeline" :more="existMore ? more : null"> - <div slot="empty"> - %fa:R comments% - %i18n:@empty% - </div> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -const fetchLimit = 10; - -export default Vue.extend({ - props: { - date: { - type: Date, - required: false, - default: null - } - }, - - data() { - return { - fetching: true, - moreFetching: false, - existMore: false, - connection: null, - connectionId: null - }; - }, - - computed: { - alone(): boolean { - return (this as any).os.i.followingCount == 0; - } - }, - - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('note', this.onNote); - this.connection.on('follow', this.onChangeFollowing); - this.connection.on('unfollow', this.onChangeFollowing); - - this.fetch(); - }, - - beforeDestroy() { - this.connection.off('note', this.onNote); - this.connection.off('follow', this.onChangeFollowing); - this.connection.off('unfollow', this.onChangeFollowing); - (this as any).os.stream.dispose(this.connectionId); - }, - - methods: { - fetch(cb?) { - this.fetching = true; - (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - (this as any).api('notes/timeline', { - 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'); - if (cb) cb(); - }, rej); - })); - }, - - more() { - this.moreFetching = true; - (this as any).api('notes/timeline', { - 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); - }, - - onChangeFollowing() { - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-timeline - > .mk-friends-maker - margin-bottom 8px -</style> diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/dashboard.vue new file mode 100644 index 0000000000..14779da650 --- /dev/null +++ b/src/client/app/mobile/views/pages/dashboard.vue @@ -0,0 +1,196 @@ +<template> +<mk-ui> + <span slot="header">%fa:home%ダッシュボード</span> + <template slot="func"> + <button @click="customizing = !customizing">%fa:cog%</button> + </template> + <main> + <template v-if="customizing"> + <header> + <select v-model="widgetAdderSelected"> + <option value="profile">プロフィール</option> + <option value="calendar">カレンダー</option> + <option value="activity">アクティビティ</option> + <option value="rss">RSSリーダー</option> + <option value="photo-stream">フォトストリーム</option> + <option value="slideshow">スライドショー</option> + <option value="version">バージョン</option> + <option value="access-log">アクセスログ</option> + <option value="server">サーバー情報</option> + <option value="donation">寄付のお願い</option> + <option value="nav">ナビゲーション</option> + <option value="tips">ヒント</option> + </select> + <button @click="addWidget">追加</button> + <p><a @click="hint">カスタマイズのヒント</a></p> + </header> + <x-draggable + :list="widgets" + :options="{ handle: '.handle', animation: 150 }" + @sort="onWidgetSort" + > + <div v-for="widget in widgets" class="customize-container" :key="widget.id"> + <header> + <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> + </header> + <div @click="widgetFunc(widget.id)"> + <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> + </div> + </div> + </x-draggable> + </template> + <template v-else> + <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/> + </template> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + components: { + XDraggable + }, + data() { + return { + showNav: false, + widgets: [], + customizing: false, + widgetAdderSelected: null + }; + }, + created() { + if ((this as any).os.i.clientSettings.mobileHome == null) { + Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{ + name: 'calendar', + id: 'a', data: {} + }, { + name: 'activity', + id: 'b', data: {} + }, { + name: 'rss', + id: 'c', data: {} + }, { + name: 'photo-stream', + id: 'd', data: {} + }, { + name: 'donation', + id: 'e', data: {} + }, { + name: 'nav', + id: 'f', data: {} + }, { + name: 'version', + id: 'g', data: {} + }]); + this.widgets = (this as any).os.i.clientSettings.mobileHome; + this.saveHome(); + } else { + this.widgets = (this as any).os.i.clientSettings.mobileHome; + } + + this.$watch('os.i.clientSettings', i => { + this.widgets = (this as any).os.i.clientSettings.mobileHome; + }, { + deep: true + }); + }, + + mounted() { + document.title = 'Misskey'; + document.documentElement.style.background = '#313a42'; + }, + + methods: { + onHomeUpdated(data) { + if (data.home) { + (this as any).os.i.clientSettings.mobileHome = data.home; + this.widgets = data.home; + } else { + const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id); + if (w != null) { + w.data = data.data; + this.$refs[w.id][0].preventSave = true; + this.$refs[w.id][0].props = w.data; + this.widgets = (this as any).os.i.clientSettings.mobileHome; + } + } + }, + hint() { + alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。'); + }, + widgetFunc(id) { + const w = this.$refs[id][0]; + if (w.func) w.func(); + }, + onWidgetSort() { + this.saveHome(); + }, + addWidget() { + const widget = { + name: this.widgetAdderSelected, + id: uuid(), + data: {} + }; + + this.widgets.unshift(widget); + this.saveHome(); + }, + removeWidget(widget) { + this.widgets = this.widgets.filter(w => w.id != widget.id); + this.saveHome(); + }, + saveHome() { + (this as any).os.i.clientSettings.mobileHome = this.widgets; + (this as any).api('i/update_mobile_home', { + home: this.widgets + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +main + margin 0 auto + max-width 500px + + @media (min-width 500px) + padding 8px + + > header + padding 8px + background #fff + + .widget + margin 8px + + .customize-container + margin 8px + background #fff + + > header + line-height 32px + background #eee + + > .handle + padding 0 8px + + > .remove + position absolute + top 0 + right 0 + padding 0 8px + line-height 32px + + > div + padding 8px + + > * + pointer-events none + +</style> diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue new file mode 100644 index 0000000000..42ed454d22 --- /dev/null +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -0,0 +1,166 @@ +<template> +<div> + <mk-friends-maker v-if="src == 'home' && alone" style="margin-bottom:8px"/> + + <mk-notes ref="timeline" :more="existMore ? more : null"> + <div slot="empty"> + %fa:R comments% + %i18n:@empty% + </div> + </mk-notes> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +const fetchLimit = 10; + +export default Vue.extend({ + props: { + src: { + type: String, + required: true + } + }, + + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null, + connectionId: null, + unreadCount: 0, + date: null + }; + }, + + computed: { + alone(): boolean { + return (this as any).os.i.followingCount == 0; + }, + + stream(): any { + return this.src == 'home' + ? (this as any).os.stream + : this.src == 'local' + ? (this as any).os.streams.localTimelineStream + : (this as any).os.streams.globalTimelineStream; + }, + + endpoint(): string { + return this.src == 'home' + ? 'notes/timeline' + : this.src == 'local' + ? 'notes/local-timeline' + : 'notes/global-timeline'; + }, + + canFetchMore(): boolean { + return !this.moreFetching && !this.fetching && this.existMore; + } + }, + + mounted() { + this.connection = this.stream.getConnection(); + this.connectionId = this.stream.use(); + + this.connection.on('note', this.onNote); + if (this.src == 'home') { + this.connection.on('follow', this.onChangeFollowing); + this.connection.on('unfollow', this.onChangeFollowing); + } + + document.addEventListener('visibilitychange', this.onVisibilitychange, false); + + this.fetch(); + }, + + beforeDestroy() { + this.connection.off('note', this.onNote); + if (this.src == 'home') { + this.connection.off('follow', this.onChangeFollowing); + this.connection.off('unfollow', this.onChangeFollowing); + } + this.stream.dispose(this.connectionId); + + document.removeEventListener('visibilitychange', this.onVisibilitychange); + }, + + methods: { + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api(this.endpoint, { + limit: fetchLimit + 1, + untilDate: this.date ? this.date.getTime() : undefined, + 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() { + if (!this.canFetchMore) return; + + this.moreFetching = true; + + (this as any).api(this.endpoint, { + 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) { + 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); + }, + + onChangeFollowing() { + this.fetch(); + }, + + focus() { + (this.$refs.timeline as any).focus(); + }, + + warp(date) { + this.date = date; + this.fetch(); + }, + + onVisibilitychange() { + if (!document.hidden) { + this.unreadCount = 0; + document.title = 'Misskey'; + } + } + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 3d94dd7ce6..3b152b3952 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -1,59 +1,37 @@ <template> <mk-ui> - <span slot="header" @click="showTl = !showTl"> - <template v-if="showTl">%fa:home%%i18n:@timeline%</template> - <template v-else>%fa:home%ウィジェット</template> + <span slot="header" @click="showNav = true"> + <span> + <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> <span style="margin-left:8px"> - <template v-if="showTl">%fa:angle-down%</template> + <template v-if="!showNav">%fa:angle-down%</template> <template v-else>%fa:angle-up%</template> </span> </span> + <template slot="func"> - <button @click="fn" v-if="showTl">%fa:pencil-alt%</button> - <button @click="customizing = !customizing" v-else>%fa:cog%</button> + <button @click="fn">%fa:pencil-alt%</button> </template> + <main> - <div class="tl"> - <mk-timeline @loaded="onLoaded" v-show="showTl"/> + <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> </div> - <div class="widgets" v-show="!showTl"> - <template v-if="customizing"> - <header> - <select v-model="widgetAdderSelected"> - <option value="profile">プロフィール</option> - <option value="calendar">カレンダー</option> - <option value="activity">アクティビティ</option> - <option value="rss">RSSリーダー</option> - <option value="photo-stream">フォトストリーム</option> - <option value="slideshow">スライドショー</option> - <option value="version">バージョン</option> - <option value="access-log">アクセスログ</option> - <option value="server">サーバー情報</option> - <option value="donation">寄付のお願い</option> - <option value="nav">ナビゲーション</option> - <option value="tips">ヒント</option> - </select> - <button @click="addWidget">追加</button> - <p><a @click="hint">カスタマイズのヒント</a></p> - </header> - <x-draggable - :list="widgets" - :options="{ handle: '.handle', animation: 150 }" - @sort="onWidgetSort" - > - <div v-for="widget in widgets" class="customize-container" :key="widget.id"> - <header> - <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> - </header> - <div @click="widgetFunc(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> - </div> - </div> - </x-draggable> - </template> - <template v-else> - <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/> - </template> + + <div class="tl"> + <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"/> </div> </main> </mk-ui> @@ -61,144 +39,38 @@ <script lang="ts"> import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import * as uuid from 'uuid'; import Progress from '../../../common/scripts/loading'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; +import XTl from './home.timeline.vue'; export default Vue.extend({ components: { - XDraggable + XTl }, + data() { return { - connection: null, - connectionId: null, - unreadCount: 0, - showTl: true, - widgets: [], - customizing: false, - widgetAdderSelected: null + src: 'home', + list: null, + showNav: false }; }, - created() { - if ((this as any).os.i.clientSettings.mobileHome == null) { - Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{ - name: 'calendar', - id: 'a', data: {} - }, { - name: 'activity', - id: 'b', data: {} - }, { - name: 'rss', - id: 'c', data: {} - }, { - name: 'photo-stream', - id: 'd', data: {} - }, { - name: 'donation', - id: 'e', data: {} - }, { - name: 'nav', - id: 'f', data: {} - }, { - name: 'version', - id: 'g', data: {} - }]); - this.widgets = (this as any).os.i.clientSettings.mobileHome; - this.saveHome(); - } else { - this.widgets = (this as any).os.i.clientSettings.mobileHome; - } - this.$watch('os.i.clientSettings', i => { - this.widgets = (this as any).os.i.clientSettings.mobileHome; - }, { - deep: true - }); - }, mounted() { document.title = 'Misskey'; document.documentElement.style.background = '#313a42'; - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('note', this.onStreamNote); - this.connection.on('mobile_home_updated', this.onHomeUpdated); - document.addEventListener('visibilitychange', this.onVisibilitychange, false); - Progress.start(); }, - beforeDestroy() { - this.connection.off('note', this.onStreamNote); - this.connection.off('mobile_home_updated', this.onHomeUpdated); - (this as any).os.stream.dispose(this.connectionId); - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }, + methods: { fn() { (this as any).apis.post(); }, + onLoaded() { Progress.done(); }, - onStreamNote(note) { - if (document.hidden && note.userId !== (this as any).os.i.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; - } - }, - onVisibilitychange() { - if (!document.hidden) { - this.unreadCount = 0; - document.title = 'Misskey'; - } - }, - onHomeUpdated(data) { - if (data.home) { - (this as any).os.i.clientSettings.mobileHome = data.home; - this.widgets = data.home; - } else { - const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id); - if (w != null) { - w.data = data.data; - this.$refs[w.id][0].preventSave = true; - this.$refs[w.id][0].props = w.data; - this.widgets = (this as any).os.i.clientSettings.mobileHome; - } - } - }, - hint() { - alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。'); - }, - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - onWidgetSort() { - this.saveHome(); - }, - addWidget() { - const widget = { - name: this.widgetAdderSelected, - id: uuid(), - data: {} - }; - this.widgets.unshift(widget); - this.saveHome(); - }, - removeWidget(widget) { - this.widgets = this.widgets.filter(w => w.id != widget.id); - this.saveHome(); - }, - saveHome() { - (this as any).os.i.clientSettings.mobileHome = this.widgets; - (this as any).api('i/update_mobile_home', { - home: this.widgets - }); - }, warp() { } @@ -208,52 +80,30 @@ export default Vue.extend({ <style lang="stylus" scoped> main + > .nav + > .bg + position fixed + z-index 10000 + top 0 + left 0 + width 100% + height 100% + background rgba(#000, 0.5) + + > .body + position fixed + z-index 10001 + top 48px + left 0 + background #fff + border-radius 8px > .tl - > .mk-timeline - max-width 600px - margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - > .widgets + max-width 600px margin 0 auto - max-width 500px + padding 8px @media (min-width 500px) - padding 8px - - > header - padding 8px - background #fff - - .widget - margin 8px - - .customize-container - margin 8px - background #fff - - > header - line-height 32px - background #eee - - > .handle - padding 0 8px - - > .remove - position absolute - top 0 - right 0 - padding 0 8px - line-height 32px - - > div - padding 8px - - > * - pointer-events none + padding 16px </style>