diff options
Diffstat (limited to 'src/client/app/desktop')
73 files changed, 2316 insertions, 1543 deletions
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts index dc89adeb86..8ddaebc072 100644 --- a/src/client/app/desktop/api/update-avatar.ts +++ b/src/client/app/desktop/api/update-avatar.ts @@ -1,4 +1,4 @@ -import OS from '../../common/mios'; +import OS from '../../mios'; import { apiUrl } from '../../config'; import CropWindow from '../views/components/crop-window.vue'; import ProgressDialog from '../views/components/progress-dialog.vue'; diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts index bc3f783e35..1a5da272bd 100644 --- a/src/client/app/desktop/api/update-banner.ts +++ b/src/client/app/desktop/api/update-banner.ts @@ -1,4 +1,4 @@ -import OS from '../../common/mios'; +import OS from '../../mios'; import { apiUrl } from '../../config'; import CropWindow from '../views/components/crop-window.vue'; import ProgressDialog from '../views/components/progress-dialog.vue'; @@ -95,7 +95,7 @@ export default (os: OS) => { multiple: false, title: '%fa:image%バナーにする画像を選択' }); - + return selectedFile .then(cropImage) .then(setBanner) diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index b3152e708b..2658a86b95 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -2,6 +2,7 @@ * Desktop Client */ +import Vue from 'vue'; import VueRouter from 'vue-router'; // Style @@ -24,8 +25,10 @@ import updateBanner from './api/update-banner'; import MkIndex from './views/pages/index.vue'; import MkUser from './views/pages/user/user.vue'; +import MkFavorites from './views/pages/favorites.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; import MkDrive from './views/pages/drive.vue'; +import MkUserList from './views/pages/user-list.vue'; import MkHomeCustomize from './views/pages/home-customize.vue'; import MkMessagingRoom from './views/pages/messaging-room.vue'; import MkNote from './views/pages/note.vue'; @@ -49,9 +52,11 @@ init(async (launch) => { routes: [ { path: '/', name: 'index', component: MkIndex }, { path: '/i/customize-home', component: MkHomeCustomize }, + { path: '/i/favorites', component: MkFavorites }, { path: '/i/messaging/:user', component: MkMessagingRoom }, { path: '/i/drive', component: MkDrive }, { path: '/i/drive/folder/:folder', component: MkDrive }, + { path: '/i/lists/:list', component: MkUserList }, { path: '/selectdrive', component: MkSelectDrive }, { path: '/search', component: MkSearch }, { path: '/othello', component: MkOthello }, diff --git a/src/client/app/desktop/style.styl b/src/client/app/desktop/style.styl index 49f71fbde7..ea48fbee3d 100644 --- a/src/client/app/desktop/style.styl +++ b/src/client/app/desktop/style.styl @@ -44,6 +44,26 @@ html height 100% background #f7f7f7 + &[data-darkmode] + background #191B22 + + &, * + &::-webkit-scrollbar-track + background-color #282C37 + + &::-webkit-scrollbar + width 6px + height 6px + + &::-webkit-scrollbar-thumb + background-color #454954 + + &:hover + background-color #535660 + + &:active + background-color $theme-color + body display flex flex-direction column diff --git a/src/client/app/desktop/ui.styl b/src/client/app/desktop/ui.styl index 5a8d1718e2..b66c8f4025 100644 --- a/src/client/app/desktop/ui.styl +++ b/src/client/app/desktop/ui.styl @@ -123,3 +123,59 @@ textarea.ui font-size 90% font-weight bold color rgba(#373a3c, 0.9) + +html[data-darkmode] + button.ui + .button.ui + color #fff + background linear-gradient(to bottom, #313543 0%, #282c37 100%) + border-color #1c2023 + + &:hover + background linear-gradient(to bottom, #2c2f3c 0%, #22262f 100%) + border-color #151a1d + + &:active + background #22262f + border-color #151a1d + + &.primary + color $theme-color-foreground + background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) + border solid 1px lighten($theme-color, 15%) + + &:hover:not(:disabled) + background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) + border-color $theme-color + + &:active:not(:disabled) + background $theme-color + border-color $theme-color + + input:not([type]).ui + input[type='text'].ui + input[type='password'].ui + input[type='email'].ui + input[type='date'].ui + input[type='number'].ui + textarea.ui + display block + padding 10px + width 100% + height 40px + font-family sans-serif + font-size 16px + color #dee4e8 + background #191b22 + border solid 1px #495156 + border-radius 4px + + &:hover + border-color #b0b0b0 + + &:focus + border-color $theme-color + + .ui.from.group + > p:first-child + color #c0c7cc diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue index 8b43536c2b..e488571070 100644 --- a/src/client/app/desktop/views/components/activity.calendar.vue +++ b/src/client/app/desktop/views/components/activity.calendar.vue @@ -61,6 +61,6 @@ svg &.day &:hover - fill rgba(0, 0, 0, 0.05) + fill rgba(#000, 0.05) </style> diff --git a/src/client/app/desktop/views/components/activity.vue b/src/client/app/desktop/views/components/activity.vue index ea33bf9ff6..bd952c39d2 100644 --- a/src/client/app/desktop/views/components/activity.vue +++ b/src/client/app/desktop/views/components/activity.vue @@ -1,14 +1,15 @@ <template> -<div class="mk-activity" :data-melt="design == 2"> - <template v-if="design == 0"> - <p class="title">%fa:chart-bar%%i18n:@title%</p> - <button @click="toggle" title="%i18n:@toggle%">%fa:sort%</button> - </template> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <template v-else> - <x-calendar v-show="view == 0" :data="[].concat(activity)"/> - <x-chart v-show="view == 1" :data="[].concat(activity)"/> - </template> +<div class="mk-activity"> + <mk-widget-container :show-header="design == 0" :naked="design == 2"> + <template slot="header">%fa:chart-bar%%i18n:@title%</template> + <button slot="func" title="%i18n:@toggle%" @click="toggle">%fa:sort%</button> + + <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <template v-else> + <x-calendar v-show="view == 0" :data="[].concat(activity)"/> + <x-chart v-show="view == 1" :data="[].concat(activity)"/> + </template> + </mk-widget-container> </div> </template> @@ -64,53 +65,14 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> -.mk-activity - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - &[data-melt] - background transparent !important - border none !important - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa +<style lang="stylus" module> +.fetching + margin 0 + padding 16px + text-align center + color #aaa - > [data-fa] - margin-right 4px + > [data-fa] + margin-right 4px </style> diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue index a99b48d195..1d8cc4f3a9 100644 --- a/src/client/app/desktop/views/components/calendar.vue +++ b/src/client/app/desktop/views/components/calendar.vue @@ -133,10 +133,10 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-calendar - color #777 - background #fff - border solid 1px rgba(0, 0, 0, 0.075) +root(isDark) + color isDark ? #c5ced6 : #777 + background isDark ? #282C37 : #fff + border solid 1px rgba(#000, 0.075) border-radius 6px &[data-melt] @@ -152,7 +152,7 @@ export default Vue.extend({ font-size 0.9em font-weight bold color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) + box-shadow 0 1px rgba(#000, 0.07) > [data-fa] margin-right 4px @@ -214,10 +214,10 @@ export default Vue.extend({ border-radius 6px &:hover > div - background rgba(0, 0, 0, 0.025) + background rgba(#000, 0.025) &:active > div - background rgba(0, 0, 0, 0.05) + background rgba(#000, 0.05) &[data-is-donichi] color #ef95a0 @@ -233,10 +233,10 @@ export default Vue.extend({ font-weight bold > div - background rgba(0, 0, 0, 0.025) + background rgba(#000, 0.025) &:active > div - background rgba(0, 0, 0, 0.05) + background rgba(#000, 0.05) &[data-today] > div @@ -249,4 +249,10 @@ export default Vue.extend({ &:active > div background darken($theme-color, 10%) +.mk-calendar[data-darkmode] + root(true) + +.mk-calendar:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue index 6359dbf1b4..843604a059 100644 --- a/src/client/app/desktop/views/components/context-menu.menu.vue +++ b/src/client/app/desktop/views/components/context-menu.menu.vue @@ -31,7 +31,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.menu +root(isDark) $width = 240px $item-height = 38px $padding = 10px @@ -46,7 +46,7 @@ export default Vue.extend({ &.divider margin-top $padding padding-top $padding - border-top solid 1px #eee + border-top solid 1px isDark ? #1c2023 : #eee &.nest > p @@ -75,7 +75,7 @@ export default Vue.extend({ margin 0 padding 0 32px 0 38px line-height $item-height - color #868C8C + color isDark ? #c8cece : #868C8C text-decoration none cursor pointer @@ -104,11 +104,17 @@ export default Vue.extend({ left $width margin-top -($padding) width $width - background #fff + background isDark ? #282c37 :#fff border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2) + box-shadow 2px 2px 8px rgba(#000, 0.2) transition visibility 0s linear 0.2s +.menu[data-darkmode] + root(true) + +.menu:not([data-darkmode]) + root(false) + </style> <style lang="stylus" module> diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue index 8bd9945840..60a33f9c93 100644 --- a/src/client/app/desktop/views/components/context-menu.vue +++ b/src/client/app/desktop/views/components/context-menu.vue @@ -54,7 +54,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.context-menu +root(isDark) $width = 240px $item-height = 38px $padding = 10px @@ -66,9 +66,15 @@ export default Vue.extend({ z-index 4096 width $width font-size 0.8em - background #fff + background isDark ? #282c37 : #fff border-radius 0 4px 4px 4px - box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2) + box-shadow 2px 2px 8px rgba(#000, 0.2) opacity 0 +.context-menu[data-darkmode] + root(true) + +.context-menu:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue index fa17e4a9d2..aff21c1754 100644 --- a/src/client/app/desktop/views/components/dialog.vue +++ b/src/client/app/desktop/views/components/dialog.vue @@ -102,7 +102,7 @@ export default Vue.extend({ left 0 width 100% height 100% - background rgba(0, 0, 0, 0.7) + background rgba(#000, 0.7) opacity 0 pointer-events none diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue index d79cb6c09c..39881711fa 100644 --- a/src/client/app/desktop/views/components/drive.file.vue +++ b/src/client/app/desktop/views/components/drive.file.vue @@ -186,7 +186,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.root.file +root(isDark) padding 8px 0 0 0 height 180px border-radius 4px @@ -195,7 +195,7 @@ export default Vue.extend({ cursor pointer &:hover - background rgba(0, 0, 0, 0.05) + background rgba(#000, 0.05) > .label &:before @@ -203,7 +203,7 @@ export default Vue.extend({ background #0b65a5 &:active - background rgba(0, 0, 0, 0.1) + background rgba(#000, 0.1) > .label &:before @@ -308,10 +308,16 @@ export default Vue.extend({ font-size 0.8em text-align center word-break break-all - color #444 + color isDark ? #fff : #444 overflow hidden > .ext opacity 0.5 +.root.file[data-darkmode] + root(true) + +.root.file:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue index 5e91048d19..973df1014d 100644 --- a/src/client/app/desktop/views/components/drive.vue +++ b/src/client/app/desktop/views/components/drive.vue @@ -577,7 +577,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-drive +root(isDark) > nav display block @@ -585,10 +585,9 @@ export default Vue.extend({ width 100% overflow auto font-size 0.9em - color #555 - background #fff - //border-bottom 1px solid #dfdfdf - box-shadow 0 1px 0 rgba(0, 0, 0, 0.05) + color isDark ? #d2d9dc : #555 + background isDark ? #282c37 : #fff + box-shadow 0 1px 0 rgba(#000, 0.05) &, * user-select none @@ -665,6 +664,7 @@ export default Vue.extend({ padding 8px height calc(100% - 38px) overflow auto + background isDark ? #191b22 : #fff &, * user-select none @@ -733,7 +733,7 @@ export default Vue.extend({ display inline-block position absolute top 0 - background-color rgba(0, 0, 0, 0.3) + background-color rgba(#000, 0.3) border-radius 100% animation sk-bounce 2.0s infinite ease-in-out @@ -770,4 +770,10 @@ export default Vue.extend({ > input display none +.mk-drive[data-darkmode] + root(true) + +.mk-drive:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/ellipsis-icon.vue b/src/client/app/desktop/views/components/ellipsis-icon.vue index c54a7db29d..4a5a0f23dc 100644 --- a/src/client/app/desktop/views/components/ellipsis-icon.vue +++ b/src/client/app/desktop/views/components/ellipsis-icon.vue @@ -14,7 +14,7 @@ display inline-block width 18px height 18px - background-color rgba(0, 0, 0, 0.3) + background-color rgba(#000, 0.3) border-radius 100% animation bounce 1.4s infinite ease-in-out both diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue index 9eb22b0fb8..60c6129f61 100644 --- a/src/client/app/desktop/views/components/follow-button.vue +++ b/src/client/app/desktop/views/components/follow-button.vue @@ -19,6 +19,7 @@ <script lang="ts"> import Vue from 'vue'; + export default Vue.extend({ props: { user: { @@ -30,6 +31,7 @@ export default Vue.extend({ default: 'compact' } }, + data() { return { wait: false, @@ -37,6 +39,7 @@ export default Vue.extend({ connectionId: null }; }, + mounted() { this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); @@ -44,13 +47,14 @@ export default Vue.extend({ this.connection.on('follow', this.onFollow); this.connection.on('unfollow', this.onUnfollow); }, + beforeDestroy() { this.connection.off('follow', this.onFollow); this.connection.off('unfollow', this.onUnfollow); (this as any).os.stream.dispose(this.connectionId); }, - methods: { + methods: { onFollow(user) { if (user.id == this.user.id) { this.user.isFollowing = user.isFollowing; @@ -94,7 +98,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-follow-button +root(isDark) display block cursor pointer padding 0 @@ -121,17 +125,17 @@ export default Vue.extend({ border-radius 8px &.follow - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 + color isDark ? #fff : #888 + background isDark ? linear-gradient(to bottom, #313543 0%, #282c37 100%) : linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) + border solid 1px isDark ? #1c2023 : #e2e2e2 &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc + background isDark ? linear-gradient(to bottom, #2c2f3c 0%, #22262f 100%) : linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) + border-color isDark ? #151a1d : #dcdcdc &:active - background #ececec - border-color #dcdcdc + background isDark ? #22262f : #ececec + border-color isDark ? #151a1d : #dcdcdc &.unfollow color $theme-color-foreground @@ -161,4 +165,10 @@ export default Vue.extend({ i margin-right 8px +.mk-follow-button[data-darkmode] + root(true) + +.mk-follow-button:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue index af5bde3ad5..3c1f8b8257 100644 --- a/src/client/app/desktop/views/components/friends-maker.vue +++ b/src/client/app/desktop/views/components/friends-maker.vue @@ -3,9 +3,7 @@ <p class="title">気になるユーザーをフォロー:</p> <div class="users" v-if="!fetching && users.length > 0"> <div class="user" v-for="user in users" :key="user.id"> - <router-link class="avatar-anchor" :to="user | userPage"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/> - </router-link> + <mk-avatar class="avatar" :user="user" target="_blank"/> <div class="body"> <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> <p class="username">@{{ user | acct }}</p> @@ -86,18 +84,13 @@ export default Vue.extend({ display block clear both - > .avatar-anchor + > .avatar display block float left margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom + width 42px + height 42px + border-radius 8px > .body float left diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue index 90e9d1b785..cae6233cd8 100644 --- a/src/client/app/desktop/views/components/home.vue +++ b/src/client/app/desktop/views/components/home.vue @@ -53,7 +53,7 @@ <div class="main"> <a @click="hint">カスタマイズのヒント</a> <div> - <mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/> + <mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/> <mk-timeline ref="tl" @loaded="onTlLoaded"/> </div> </div> @@ -63,7 +63,7 @@ <component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/> </div> <div class="main"> - <mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/> + <mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/> <mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/> <mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/> </div> @@ -81,6 +81,7 @@ export default Vue.extend({ components: { XDraggable }, + props: { customize: { type: Boolean, @@ -91,61 +92,43 @@ export default Vue.extend({ default: 'timeline' } }, + data() { return { connection: null, connectionId: null, widgetAdderSelected: null, - trash: [], - widgets: { - left: [], - right: [] - } + trash: [] }; }, + computed: { - home: { - get(): any[] { - //#region 互換性のため - (this as any).os.i.clientSettings.home.forEach(w => { - if (w.name == 'rss-reader') w.name = 'rss'; - if (w.name == 'user-recommendation') w.name = 'users'; - if (w.name == 'recommended-polls') w.name = 'polls'; - }); - //#endregion - return (this as any).os.i.clientSettings.home; - }, - set(value) { - (this as any).os.i.clientSettings.home = value; - } + home(): any[] { + return this.$store.state.settings.data.home; }, left(): any[] { return this.home.filter(w => w.place == 'left'); }, right(): any[] { return this.home.filter(w => w.place == 'right'); + }, + widgets(): any { + return { + left: this.left, + right: this.right + }; } }, - created() { - this.widgets.left = this.left; - this.widgets.right = this.right; - this.$watch('os.i.clientSettings', i => { - this.widgets.left = this.left; - this.widgets.right = this.right; - }, { - deep: true - }); - }, + mounted() { this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); - - this.connection.on('home_updated', this.onHomeUpdated); }, + beforeDestroy() { - this.connection.off('home_updated', this.onHomeUpdated); (this as any).os.stream.dispose(this.connectionId); }, + methods: { hint() { (this as any).apis.dialog({ @@ -159,56 +142,44 @@ export default Vue.extend({ }] }); }, + onTlLoaded() { this.$emit('loaded'); }, - onHomeUpdated(data) { - if (data.home) { - (this as any).os.i.clientSettings.home = data.home; - this.widgets.left = data.home.filter(w => w.place == 'left'); - this.widgets.right = data.home.filter(w => w.place == 'right'); - } else { - const w = (this as any).os.i.clientSettings.home.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.left = (this as any).os.i.clientSettings.home.filter(w => w.place == 'left'); - this.widgets.right = (this as any).os.i.clientSettings.home.filter(w => w.place == 'right'); - } - } - }, + onWidgetContextmenu(widgetId) { const w = (this.$refs[widgetId] as any)[0]; if (w.func) w.func(); }, + onWidgetSort() { this.saveHome(); }, + onTrash(evt) { this.saveHome(); }, + addWidget() { - const widget = { + this.$store.dispatch('settings/addHomeWidget', { name: this.widgetAdderSelected, id: uuid(), place: 'left', data: {} - }; - - this.widgets.left.unshift(widget); - this.saveHome(); + }); }, + saveHome() { const left = this.widgets.left; const right = this.widgets.right; - this.home = left.concat(right); + this.$store.commit('settings/setHome', left.concat(right)); left.forEach(w => w.place = 'left'); right.forEach(w => w.place = 'right'); (this as any).api('i/update_home', { home: this.home }); }, + warp(date) { (this.$refs.tl as any).warp(date); } @@ -219,7 +190,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-home +root(isDark) display block &[data-customize] @@ -249,8 +220,9 @@ export default Vue.extend({ left 0 width 100% height 48px - background #f7f7f7 - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) + color isDark ? #fff : #000 + background isDark ? #313543 : #f7f7f7 + box-shadow 0 1px 1px rgba(#000, 0.075) > a display block @@ -278,7 +250,7 @@ export default Vue.extend({ > div display flex margin 0 auto - max-width 1200px - 32px + max-width 1220px - 32px > div width 50% @@ -289,7 +261,7 @@ export default Vue.extend({ line-height 48px &.trash - border-left solid 1px #ddd + border-left solid 1px isDark ? #1c2023 : #ddd > div width 100% @@ -309,7 +281,7 @@ export default Vue.extend({ display flex justify-content center margin 0 auto - max-width 1200px + max-width 1220px > * .customize-container @@ -329,7 +301,7 @@ export default Vue.extend({ .mk-post-form margin-bottom 16px - border solid 1px #e5e5e5 + border solid 1px rgba(#000, 0.075) border-radius 4px > *:not(.main) @@ -357,4 +329,10 @@ export default Vue.extend({ max-width 700px margin 0 auto +.mk-home[data-darkmode] + root(true) + +.mk-home:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts index 4f61f43692..f58d0706df 100644 --- a/src/client/app/desktop/views/components/index.ts +++ b/src/client/app/desktop/views/components/index.ts @@ -28,6 +28,7 @@ import friendsMaker from './friends-maker.vue'; import followers from './followers.vue'; import following from './following.vue'; import usersList from './users-list.vue'; +import userListTimeline from './user-list-timeline.vue'; import widgetContainer from './widget-container.vue'; Vue.component('mk-ui', ui); @@ -58,4 +59,5 @@ Vue.component('mk-friends-maker', friendsMaker); Vue.component('mk-followers', followers); Vue.component('mk-following', following); Vue.component('mk-users-list', usersList); +Vue.component('mk-user-list-timeline', userListTimeline); Vue.component('mk-widget-container', widgetContainer); diff --git a/src/client/app/desktop/views/components/media-image-dialog.vue b/src/client/app/desktop/views/components/media-image-dialog.vue index dec140d1c9..026522d907 100644 --- a/src/client/app/desktop/views/components/media-image-dialog.vue +++ b/src/client/app/desktop/views/components/media-image-dialog.vue @@ -52,7 +52,7 @@ export default Vue.extend({ left 0 width 100% height 100% - background rgba(0, 0, 0, 0.7) + background rgba(#000, 0.7) > img position fixed diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue index 51309a0578..e5803cc36e 100644 --- a/src/client/app/desktop/views/components/media-image.vue +++ b/src/client/app/desktop/views/components/media-image.vue @@ -14,12 +14,20 @@ import Vue from 'vue'; import MkMediaImageDialog from './media-image-dialog.vue'; export default Vue.extend({ - props: ['image'], + props: { + image: { + type: Object, + required: true + }, + raw: { + default: false + } + }, computed: { style(): any { return { 'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', - 'background-image': `url(${this.image.url}?thumbnail&size=512)` + 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)` }; } }, @@ -31,7 +39,7 @@ export default Vue.extend({ const xp = mouseX / this.$el.offsetWidth * 100; const yp = mouseY / this.$el.offsetHeight * 100; this.$el.style.backgroundPosition = xp + '% ' + yp + '%'; - this.$el.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")'; + this.$el.style.backgroundImage = `url("${this.image.url}")`; }, onMouseleave() { diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue index cbf862cd1c..959cefa42c 100644 --- a/src/client/app/desktop/views/components/media-video-dialog.vue +++ b/src/client/app/desktop/views/components/media-video-dialog.vue @@ -54,7 +54,7 @@ export default Vue.extend({ left 0 width 100% height 100% - background rgba(0, 0, 0, 0.7) + background rgba(#000, 0.7) > video position fixed diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue index 4fd955a821..3635941e64 100644 --- a/src/client/app/desktop/views/components/media-video.vue +++ b/src/client/app/desktop/views/components/media-video.vue @@ -52,6 +52,7 @@ export default Vue.extend({ width 100% height 100% border-radius 4px + .mk-media-video-thumbnail display flex justify-content center diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue index fc3a7af75d..66bdab5c08 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/> @@ -85,7 +85,7 @@ export default Vue.extend({ .mk-mentions background #fff - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px > header @@ -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/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue index 16bc2a1d98..24550c4e94 100644 --- a/src/client/app/desktop/views/components/note-detail.sub.vue +++ b/src/client/app/desktop/views/components/note-detail.sub.vue @@ -1,8 +1,6 @@ <template> <div class="sub" :title="title"> - <router-link class="avatar-anchor" :to="note.user | userPage"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> - </router-link> + <mk-avatar class="avatar" :user="note.user"/> <div class="main"> <header> <div class="left"> @@ -16,8 +14,11 @@ </div> </header> <div class="body"> - <mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/> - <div class="media" v-if="note.media > 0"> + <div class="text"> + <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span> + <mk-note-html v-if="note.text" :text="note.text" :i="os.i"/> + </div> + <div class="media" v-if="note.mediaIds.length > 0"> <mk-media-list :media-list="note.media"/> </div> </div> @@ -40,10 +41,10 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.sub +root(isDark) margin 0 padding 20px 32px - background #fdfdfd + background isDark ? #21242d : #fdfdfd &:after content "" @@ -54,18 +55,13 @@ export default Vue.extend({ > .main > footer > button color #888 - > .avatar-anchor + > .avatar display block float left margin 0 16px 0 0 - - > .avatar - display block - width 44px - height 44px - margin 0 - border-radius 4px - vertical-align bottom + width 44px + height 44px + border-radius 4px > .main float left @@ -87,7 +83,7 @@ export default Vue.extend({ display inline margin 0 padding 0 - color #777 + color isDark ? #fff : #777 font-size 1em font-weight 700 text-align left @@ -99,24 +95,29 @@ export default Vue.extend({ > .username text-align left margin 0 0 0 8px - color #ccc + color isDark ? #606984 : #ccc > .right float right > .time font-size 0.9em - color #c0c0c0 + color isDark ? #606984 : #c0c0c0 -</style> + > .body + > .text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1em + color isDark ? #959ba7 : #717171 + +.sub[data-darkmode] + root(true) + +.sub:not([data-darkmode]) + root(false) -<style lang="stylus" module> -.text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1em - color #717171 </style> diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index b62a7cfd61..a0e3915149 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -18,18 +18,14 @@ </div> <div class="renote" v-if="isRenote"> <p> - <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" :user="note.user"/> %fa:retweet% <router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link> がRenote </p> </div> <article> - <router-link class="avatar-anchor" :to="p.user | userPage"> - <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> - </router-link> + <mk-avatar class="avatar" :user="p.user"/> <header> <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> <span class="username">@{{ p.user | acct }}</span> @@ -38,9 +34,12 @@ </router-link> </header> <div class="body"> - <mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/> + <div class="text"> + <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span> + <mk-note-html v-if="p.text" :text="p.text" :i="os.i"/> + </div> <div class="media" v-if="p.media.length > 0"> - <mk-media-list :media-list="p.media"/> + <mk-media-list :media-list="p.media" :raw="true"/> </div> <mk-poll v-if="p.poll" :note="p"/> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> @@ -56,7 +55,9 @@ <footer> <mk-reactions-viewer :note="p"/> <button @click="reply" title="返信"> - %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + <template v-if="p.reply">%fa:reply-all%</template> + <template v-else>%fa:reply%</template> + <p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> </button> <button @click="renote" title="Renote"> %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> @@ -154,7 +155,7 @@ export default Vue.extend({ // Draw map if (this.p.geo) { - const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true; if (shouldShowMap) { (this as any).os.getGoogleMaps().then(maps => { const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); @@ -212,13 +213,13 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-note-detail - margin 0 +root(isDark) + margin 0 auto padding 0 overflow hidden text-align left - background #fff - border solid 1px rgba(0, 0, 0, 0.1) + background isDark ? #282C37 : #fff + border solid 1px rgba(#000, 0.1) border-radius 8px > .read-more @@ -230,44 +231,39 @@ export default Vue.extend({ text-align center color #999 cursor pointer - background #fafafa + background isDark ? #21242d : #fafafa outline none border none - border-bottom solid 1px #eef0f2 + border-bottom solid 1px isDark ? #1c2023 : #eef0f2 border-radius 6px 6px 0 0 &:hover - background #f6f6f6 + background isDark ? #2e3440 : #f6f6f6 &:active - background #f0f0f0 + background isDark ? #21242b : #f0f0f0 &:disabled - color #ccc + color isDark ? #21242b : #ccc > .context > * - border-bottom 1px solid #eef0f2 + border-bottom 1px solid isDark ? #1c2023 : #eef0f2 > .renote color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%) > p margin 0 padding 16px 32px - .avatar-anchor + .avatar display inline-block - - .avatar - vertical-align bottom - min-width 28px - min-height 28px - max-width 28px - max-height 28px - margin 0 8px 0 0 - border-radius 6px + width 28px + height 28px + margin 0 8px 0 0 + border-radius 6px [data-fa] margin-right 4px @@ -279,7 +275,7 @@ export default Vue.extend({ padding-top 8px > .reply-to - border-bottom 1px solid #eef0f2 + border-bottom 1px solid isDark ? #1c2023 : #eef0f2 > article padding 28px 32px 18px 32px @@ -290,21 +286,13 @@ export default Vue.extend({ clear both &:hover - > .main > footer > button - color #888 + > footer > button + color isDark ? #707b97 : #888 - > .avatar-anchor - display block + > .avatar width 60px height 60px - - > .avatar - display block - width 60px - height 60px - margin 0 - border-radius 8px - vertical-align bottom + border-radius 8px > header position absolute @@ -316,7 +304,7 @@ export default Vue.extend({ display inline-block margin 0 line-height 24px - color #777 + color isDark ? #fff : #627079 font-size 18px font-weight 700 text-align left @@ -329,18 +317,27 @@ export default Vue.extend({ display block text-align left margin 0 - color #ccc + color isDark ? #606984 : #ccc > .time position absolute top 0 right 32px font-size 1em - color #c0c0c0 + color isDark ? #606984 : #c0c0c0 > .body padding 8px 0 + > .text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.5em + color isDark ? #fff : #717171 + > .renote margin 8px 0 @@ -402,11 +399,11 @@ export default Vue.extend({ background transparent border none font-size 1em - color #ddd + color isDark ? #606984 : #ccc cursor pointer &:hover - color #666 + color isDark ? #9198af : #666 > .count display inline @@ -418,17 +415,12 @@ export default Vue.extend({ > .replies > * - border-top 1px solid #eef0f2 + border-top 1px solid isDark ? #1c2023 : #eef0f2 -</style> +.mk-note-detail[data-darkmode] + root(true) + +.mk-note-detail:not([data-darkmode]) + root(false) -<style lang="stylus" module> -.text - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.5em - color #717171 </style> diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue index ff3ecadc20..d04abfc5a7 100644 --- a/src/client/app/desktop/views/components/note-preview.vue +++ b/src/client/app/desktop/views/components/note-preview.vue @@ -1,8 +1,6 @@ <template> <div class="mk-note-preview" :title="title"> - <router-link class="avatar-anchor" :to="note.user | userPage"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> - </router-link> + <mk-avatar class="avatar" :user="note.user"/> <div class="main"> <header> <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> @@ -33,31 +31,21 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-note-preview +root(isDark) font-size 0.9em - background #fff &:after content "" display block clear both - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor + > .avatar display block float left margin 0 16px 0 0 - - > .avatar - display block - width 52px - height 52px - margin 0 - border-radius 8px - vertical-align bottom + width 52px + height 52px + border-radius 8px > .main float left @@ -65,12 +53,13 @@ export default Vue.extend({ > header display flex + align-items baseline white-space nowrap > .name margin 0 .5em 0 0 padding 0 - color #607073 + color isDark ? #fff : #607073 font-size 1em font-weight bold text-decoration none @@ -81,11 +70,11 @@ export default Vue.extend({ > .username margin 0 .5em 0 0 - color #d1d8da + color isDark ? #606984 : #d1d8da > .time margin-left auto - color #b2b8bb + color isDark ? #606984 : #b2b8bb > .body @@ -94,6 +83,12 @@ export default Vue.extend({ margin 0 padding 0 font-size 1.1em - color #717171 + color isDark ? #959ba7 : #717171 + +.mk-note-preview[data-darkmode] + root(true) + +.mk-note-preview:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue index e854785783..575d605203 100644 --- a/src/client/app/desktop/views/components/notes.note.sub.vue +++ b/src/client/app/desktop/views/components/notes.note.sub.vue @@ -1,15 +1,22 @@ <template> <div class="sub" :title="title"> - <router-link class="avatar-anchor" :to="note.user | userPage"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> - </router-link> + <mk-avatar class="avatar" :user="note.user"/> <div class="main"> <header> <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> <span class="username">@{{ note.user | acct }}</span> - <router-link class="created-at" :to="note | notePage"> - <mk-time :time="note.createdAt"/> - </router-link> + <div class="info"> + <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> + <router-link class="created-at" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + <span class="visibility" v-if="note.visibility != 'public'"> + <template v-if="note.visibility == 'home'">%fa:home%</template> + <template v-if="note.visibility == 'followers'">%fa:unlock%</template> + <template v-if="note.visibility == 'specified'">%fa:envelope%</template> + <template v-if="note.visibility == 'private'">%fa:lock%</template> + </span> + </div> </header> <div class="body"> <mk-sub-note-content class="text" :note="note"/> @@ -33,32 +40,24 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.sub +root(isDark) margin 0 - padding 16px + padding 16px 32px font-size 0.9em + background isDark ? #21242d : #fcfcfc &:after content "" display block clear both - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor + > .avatar display block float left margin 0 14px 0 0 - - > .avatar - display block - width 52px - height 52px - margin 0 - border-radius 8px - vertical-align bottom + width 52px + height 52px + border-radius 8px > .main float left @@ -66,6 +65,7 @@ export default Vue.extend({ > header display flex + align-items baseline margin-bottom 2px white-space nowrap line-height 21px @@ -75,7 +75,7 @@ export default Vue.extend({ margin 0 .5em 0 0 padding 0 overflow hidden - color #607073 + color isDark ? #fff : #607073 font-size 1em font-weight bold text-decoration none @@ -86,23 +86,40 @@ export default Vue.extend({ > .username margin 0 .5em 0 0 - color #d1d8da + color isDark ? #606984 : #d1d8da - > .created-at + > .info margin-left auto - color #b2b8bb + font-size 0.9em + + > * + color isDark ? #606984 : #b2b8bb + + > .mobile + margin-right 6px + + > .visibility + margin-left 6px > .body + max-height 128px + overflow hidden > .text cursor default margin 0 padding 0 font-size 1.1em - color #717171 + color isDark ? #959ba7 : #717171 pre max-height 120px font-size 80% +.sub[data-darkmode] + root(true) + +.sub:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 326ec4dc89..057c3c0956 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -1,24 +1,18 @@ <template> <div class="note" tabindex="-1" :title="title" @keydown="onKeydown"> - <div class="reply-to" v-if="p.reply"> + <div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)"> <x-sub :note="p.reply"/> </div> <div class="renote" v-if="isRenote"> - <p> - <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> - </router-link> - %fa:retweet% - <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span> - <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a> - <span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span> - </p> + <mk-avatar class="avatar" :user="note.user"/> + %fa:retweet% + <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span> + <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a> + <span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span> <mk-time :time="note.createdAt"/> </div> <article> - <router-link class="avatar-anchor" :to="p.user | userPage"> - <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> - </router-link> + <mk-avatar class="avatar" :user="p.user"/> <div class="main"> <header> <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> @@ -30,35 +24,50 @@ <router-link class="created-at" :to="p | notePage"> <mk-time :time="p.createdAt"/> </router-link> + <span class="visibility" v-if="p.visibility != 'public'"> + <template v-if="p.visibility == 'home'">%fa:home%</template> + <template v-if="p.visibility == 'followers'">%fa:unlock%</template> + <template v-if="p.visibility == 'specified'">%fa:envelope%</template> + <template v-if="p.visibility == 'private'">%fa:lock%</template> + </span> </div> </header> <div class="body"> <p class="channel" v-if="p.channel"> <a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>: </p> - <div class="text"> - <a class="reply" v-if="p.reply">%fa:reply%</a> - <mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/> - <a class="rp" v-if="p.renote">RP:</a> - </div> - <div class="media" v-if="p.media.length > 0"> - <mk-media-list :media-list="p.media"/> - </div> - <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> - <div class="tags" v-if="p.tags && p.tags.length > 0"> - <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> - </div> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> - <div class="map" v-if="p.geo" ref="map"></div> - <div class="renote" v-if="p.renote"> - <mk-note-preview :note="p.renote"/> + <p v-if="p.cw != null" class="cw"> + <span class="text" v-if="p.cw != ''">{{ p.cw }}</span> + <span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span> + </p> + <div class="content" v-show="p.cw == null || showContent"> + <div class="text"> + <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span> + <a class="reply" v-if="p.reply">%fa:reply%</a> + <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/> + <a class="rp" v-if="p.renote">RP:</a> + </div> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> </div> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> </div> <footer> <mk-reactions-viewer :note="p" ref="reactionsViewer"/> <button @click="reply" title="%i18n:@reply%"> - %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + <template v-if="p.reply">%fa:reply-all%</template> + <template v-else>%fa:reply%</template> + <p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> </button> <button @click="renote" title="%i18n:@renote%"> %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> @@ -113,6 +122,7 @@ export default Vue.extend({ data() { return { + showContent: false, isDetailOpened: false, connection: null, connectionId: null @@ -168,7 +178,7 @@ export default Vue.extend({ // Draw map if (this.p.geo) { - const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true; if (shouldShowMap) { (this as any).os.getGoogleMaps().then(maps => { const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); @@ -289,20 +299,21 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.note +root(isDark) margin 0 padding 0 - background #fff - border-bottom solid 1px #eaeaea - - &:first-child - border-top-left-radius 6px - border-top-right-radius 6px + background isDark ? #282C37 : #fff + border-bottom solid 1px isDark ? #1c2023 : #eaeaea - > .renote + &[data-round] + &:first-child border-top-left-radius 6px border-top-right-radius 6px + > .renote + border-top-left-radius 6px + border-top-right-radius 6px + &:last-of-type border-bottom none @@ -321,47 +332,45 @@ export default Vue.extend({ border-radius 4px > .renote + display flex + align-items center + padding 16px 32px + line-height 28px color #9dbb00 - background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%) - > p - margin 0 - padding 16px 32px - line-height 28px + .avatar + display inline-block + width 28px + height 28px + margin 0 8px 0 0 + border-radius 6px - .avatar-anchor - display inline-block + [data-fa] + margin-right 4px - .avatar - vertical-align bottom - width 28px - height 28px - margin 0 8px 0 0 - border-radius 6px + > span + flex-shrink 0 - [data-fa] - margin-right 4px + &:last-of-type + margin-right 8px - .name - font-weight bold + .name + overflow hidden + flex-shrink 1 + text-overflow ellipsis + white-space nowrap + font-weight bold > .mk-time - position absolute - top 16px - right 32px + display block + margin-left auto + flex-shrink 0 font-size 0.9em - line-height 28px & + article padding-top 8px - > .reply-to - padding 0 16px - background rgba(0, 0, 0, 0.0125) - - > .mk-note-preview - background transparent - > article padding 28px 32px 18px 32px @@ -372,31 +381,26 @@ export default Vue.extend({ &:hover > .main > footer > button - color #888 + color isDark ? #707b97 : #888 - > .avatar-anchor + > .avatar display block float left margin 0 16px 10px 0 + width 58px + height 58px + border-radius 8px //position -webkit-sticky //position sticky //top 74px - > .avatar - display block - width 58px - height 58px - margin 0 - border-radius 8px - vertical-align bottom - > .main float left width calc(100% - 74px) > header display flex - align-items center + align-items baseline margin-bottom 4px white-space nowrap @@ -405,7 +409,7 @@ export default Vue.extend({ margin 0 .5em 0 0 padding 0 overflow hidden - color #627079 + color isDark ? #fff : #627079 font-size 1em font-weight bold text-decoration none @@ -418,114 +422,156 @@ export default Vue.extend({ margin 0 .5em 0 0 padding 1px 6px font-size 12px - color #aaa - border solid 1px #ddd + color isDark ? #758188 : #aaa + border solid 1px isDark ? #57616f : #ddd border-radius 3px > .username margin 0 .5em 0 0 - color #ccc + overflow hidden + text-overflow ellipsis + color isDark ? #606984 : #ccc > .info margin-left auto font-size 0.9em + > * + color isDark ? #606984 : #c0c0c0 + > .mobile margin-right 8px - color #ccc > .app margin-right 8px padding-right 8px - color #ccc border-right solid 1px #eaeaea - > .created-at - color #c0c0c0 + > .visibility + margin-left 8px > .body - > .text + > .cw cursor default display block margin 0 padding 0 overflow-wrap break-word font-size 1.1em - color #717171 - - >>> .quote - margin 8px - padding 6px 12px - color #aaa - border-left solid 3px #eee + color isDark ? #fff : #717171 - > .reply + > .text margin-right 8px - color #717171 - > .rp - margin-left 4px - font-style oblique - color #a0bf46 + > .toggle + display inline-block + padding 4px 8px + font-size 0.7em + color isDark ? #393f4f : #fff + background isDark ? #687390 : #b1b9c1 + border-radius 2px + cursor pointer + user-select none - > .location - margin 4px 0 - font-size 12px - color #ccc + &:hover + background isDark ? #707b97 : #bbc4ce - > .map - width 100% - height 300px + > .content - &:empty - display none + > .text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.1em + color isDark ? #fff : #717171 - > .tags - margin 4px 0 0 0 + >>> .title + display block + margin-bottom 4px + padding 4px + font-size 90% + text-align center + background isDark ? #2f3944 : #eef1f3 + border-radius 4px - > * - display inline-block - margin 0 8px 0 0 - padding 2px 8px 2px 16px - font-size 90% - color #8d969e - background #edf0f3 - border-radius 4px + >>> .code + margin 8px 0 - &:before - content "" - display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background #fff - border-radius 100% + >>> .quote + margin 8px + padding 6px 12px + color isDark ? #6f808e : #aaa + border-left solid 3px isDark ? #637182 : #eee - &:hover - text-decoration none - background #e2e7ec + > .reply + margin-right 8px + color isDark ? #99abbf : #717171 - .mk-url-preview - margin-top 8px + > .rp + margin-left 4px + font-style oblique + color #a0bf46 - > .channel - margin 0 + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 300px - > .mk-poll - font-size 80% + &:empty + display none - > .renote - margin 8px 0 + > .tags + margin 4px 0 0 0 - > .mk-note-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + &:hover + text-decoration none + background #e2e7ec + + .mk-url-preview + margin-top 8px + + > .channel + margin 0 + + > .mk-poll + font-size 80% + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px isDark ? #4e945e : #c0dac6 + border-radius 8px > footer > button @@ -533,13 +579,13 @@ export default Vue.extend({ padding 0 8px line-height 32px font-size 1em - color #ddd + color isDark ? #606984 : #ddd background transparent border none cursor pointer &:hover - color #666 + color isDark ? #9198af : #666 > .count display inline @@ -556,7 +602,13 @@ export default Vue.extend({ > .detail padding-top 4px - background rgba(0, 0, 0, 0.0125) + background rgba(#000, 0.0125) + +.note[data-darkmode] + root(true) + +.note:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index b5f6957a16..7e80e6f74a 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -1,32 +1,65 @@ <template> <div class="mk-notes"> - <template v-for="(note, i) in _notes"> - <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> - <p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> - <span>%fa:angle-up%{{ note._datetext }}</span> - <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> - </p> - </template> - <footer> - <slot name="footer"></slot> + <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> + + <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> + + <div v-if="!fetching && requestInitPromise != null"> + <p>読み込みに失敗しました。</p> + <button @click="resolveInitPromise">リトライ</button> + </div> + + <transition-group name="mk-notes" class="transition"> + <template v-for="(note, i) in _notes"> + <x-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"> + <span>%fa:angle-up%{{ note._datetext }}</span> + <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> + </p> + </template> + </transition-group> + + <footer v-if="more"> + <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <template v-if="!moreFetching">%i18n:@load-more%</template> + <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> + </button> </footer> </div> </template> <script lang="ts"> import Vue from 'vue'; +import { url } from '../../../config'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + import XNote from './notes.note.vue'; +const displayLimit = 30; + export default Vue.extend({ components: { XNote }, + props: { - notes: { - type: Array, - default: () => [] + more: { + type: Function, + required: false } }, + + data() { + return { + requestInitPromise: null as () => Promise<any[]>, + notes: [], + queue: [], + unreadCount: 0, + fetching: true, + moreFetching: false + }; + }, + computed: { _notes(): any[] { return (this.notes as any).map(note => { @@ -38,52 +71,202 @@ 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); + }, + methods: { + isScrollTop() { + return window.scrollY <= 8; + }, + focus() { (this.$el as any).children[0].focus(); }, + onNoteUpdated(i, note) { Vue.set((this as any).notes, i, note); + }, + + init(promiseGenerator: () => Promise<any[]>) { + this.requestInitPromise = promiseGenerator; + this.resolveInitPromise(); + }, + + resolveInitPromise() { + this.queue = []; + this.notes = []; + this.fetching = true; + + const promise = this.requestInitPromise(); + + promise.then(notes => { + this.notes = notes; + this.requestInitPromise = null; + this.fetching = false; + }, e => { + this.fetching = false; + }); + }, + + prepend(note, silent = false) { + //#region 弾く + const isMyNote = note.userId == (this as any).os.i.id; + const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + + if ((this as any).clientSettings.showMyRenotes === false) { + if (isMyNote && isPureRenote) { + return; + } + } + + if ((this as any).clientSettings.showRenotedMyNotes === false) { + if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) { + return; + } + } + //#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); + + // サウンドを再生する + if ((this as any).os.isEnableSounds && !silent) { + const sound = new Audio(`${url}/assets/post.mp3`); + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; + sound.play(); + } + + // オーバーフローしたら古い投稿は捨てる + if (this.notes.length >= displayLimit) { + this.notes = this.notes.slice(0, displayLimit); + } + } else { + this.queue.push(note); + } + }, + + append(note) { + this.notes.push(note); + }, + + tail() { + return this.notes[this.notes.length - 1]; + }, + + releaseQueue() { + this.queue.forEach(n => this.prepend(n, true)); + this.queue = []; + }, + + async loadMore() { + if (this.more == null) return; + if (this.moreFetching) return; + + this.moreFetching = true; + await this.more(); + 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).clientSettings.fetchOnScroll !== false) { + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 8) this.loadMore(); + } } } }); </script> <style lang="stylus" scoped> -.mk-notes +@import '~const.styl' + +root(isDark) + .transition + .mk-notes-enter + .mk-notes-leave-to + opacity 0 + transform translateY(-30px) + + > * + transition transform .3s ease, opacity .3s ease - > .date - display block - margin 0 - line-height 32px - font-size 14px - text-align center - color #aaa - background #fdfdfd - border-bottom solid 1px #eaeaea + > .date + display block + margin 0 + line-height 32px + font-size 14px + text-align center + color isDark ? #666b79 : #aaa + background isDark ? #242731 : #fdfdfd + border-bottom solid 1px isDark ? #1c2023 : #eaeaea - span - margin 0 16px + span + margin 0 16px - [data-fa] - margin-right 8px + [data-fa] + margin-right 8px + + > .newer-indicator + position -webkit-sticky + position sticky + z-index 100 + height 3px + background $theme-color > footer - > * + > button display block margin 0 padding 16px width 100% text-align center color #ccc - border-top solid 1px #eaeaea - border-bottom-left-radius 4px - border-bottom-right-radius 4px + background isDark ? #282C37 : #fff + border-top solid 1px isDark ? #1c2023 : #eaeaea + border-bottom-left-radius 6px + border-bottom-right-radius 6px - > button &:hover - background #f5f5f5 + background isDark ? #2e3440 : #f5f5f5 &:active - background #eee + background isDark ? #21242b : #eee + +.mk-notes[data-darkmode] + root(true) + +.mk-notes:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index 413a87755a..7923d1a62d 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -1,96 +1,84 @@ <template> <div class="mk-notifications"> <div class="notifications" v-if="notifications.length != 0"> - <template v-for="(notification, i) in _notifications"> - <div class="notification" :class="notification.type" :key="notification.id"> - <mk-time :time="notification.createdAt"/> - <template v-if="notification.type == 'reaction'"> - <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p> - <mk-reaction-icon :reaction="notification.reaction"/> - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> - </p> - <router-link class="note-ref" :to="notification.note | notePage"> - %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% - </router-link> - </div> - </template> - <template v-if="notification.type == 'renote'"> - <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> - <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:retweet% - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> - </p> - <router-link class="note-ref" :to="notification.note | notePage"> - %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% - </router-link> - </div> - </template> - <template v-if="notification.type == 'quote'"> - <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> - <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:quote-left% - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> - </p> - <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> - </div> - </template> - <template v-if="notification.type == 'follow'"> - <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:user-plus% - <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> - </p> - </div> - </template> - <template v-if="notification.type == 'reply'"> - <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> - <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:reply% - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> - </p> - <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> - </div> - </template> - <template v-if="notification.type == 'mention'"> - <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> - <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:at% - <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> - </p> - <a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a> - </div> - </template> - <template v-if="notification.type == 'poll_vote'"> - <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> - </router-link> - <div class="text"> - <p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p> - <router-link class="note-ref" :to="notification.note | notePage"> - %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% - </router-link> - </div> - </template> - </div> - <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> - <span>%fa:angle-up%{{ notification._datetext }}</span> - <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> - </p> - </template> + <transition-group name="mk-notifications" class="transition"> + <template v-for="(notification, i) in _notifications"> + <div class="notification" :class="notification.type" :key="notification.id"> + <mk-time :time="notification.createdAt"/> + <template v-if="notification.type == 'reaction'"> + <mk-avatar class="avatar" :user="notification.user"/> + <div class="text"> + <p> + <mk-reaction-icon :reaction="notification.reaction"/> + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% + </router-link> + </div> + </template> + <template v-if="notification.type == 'renote'"> + <mk-avatar class="avatar" :user="notification.note.user"/> + <div class="text"> + <p>%fa:retweet% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% + </router-link> + </div> + </template> + <template v-if="notification.type == 'quote'"> + <mk-avatar class="avatar" :user="notification.note.user"/> + <div class="text"> + <p>%fa:quote-left% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> + </div> + </template> + <template v-if="notification.type == 'follow'"> + <mk-avatar class="avatar" :user="notification.user"/> + <div class="text"> + <p>%fa:user-plus% + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> + </p> + </div> + </template> + <template v-if="notification.type == 'reply'"> + <mk-avatar class="avatar" :user="notification.note.user"/> + <div class="text"> + <p>%fa:reply% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link> + </div> + </template> + <template v-if="notification.type == 'mention'"> + <mk-avatar class="avatar" :user="notification.note.user"/> + <div class="text"> + <p>%fa:at% + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> + </p> + <a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a> + </div> + </template> + <template v-if="notification.type == 'poll_vote'"> + <mk-avatar class="avatar" :user="notification.user"/> + <div class="text"> + <p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p> + <router-link class="note-ref" :to="notification.note | notePage"> + %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% + </router-link> + </div> + </template> + </div> + <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'"> + <span>%fa:angle-up%{{ notification._datetext }}</span> + <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> + </p> + </template> + </transition-group> </div> <button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }} @@ -185,111 +173,116 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-notifications - > .notifications - > .notification - margin 0 - padding 16px - overflow-wrap break-word - font-size 0.9em - border-bottom solid 1px rgba(0, 0, 0, 0.05) +root(isDark) + .transition + .mk-notifications-enter + .mk-notifications-leave-to + opacity 0 + transform translateY(-30px) - &:last-child - border-bottom none + > * + transition transform .3s ease, opacity .3s ease - > .mk-time - display inline - position absolute - top 16px - right 12px - vertical-align top - color rgba(0, 0, 0, 0.6) - font-size small + > .notifications + > * + > .notification + margin 0 + padding 16px + overflow-wrap break-word + font-size 0.9em + border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05) - &:after - content "" - display block - clear both + &:last-child + border-bottom none - > .avatar-anchor - display block - float left - position -webkit-sticky - position sticky - top 16px + > .mk-time + display inline + position absolute + top 16px + right 12px + vertical-align top + color isDark ? #606984 : rgba(#000, 0.6) + font-size small + + &:after + content "" + display block + clear both - > img + > .avatar display block - min-width 36px - min-height 36px - max-width 36px - max-height 36px + float left + position -webkit-sticky + position sticky + top 16px + width 36px + height 36px border-radius 6px - > .text - float right - width calc(100% - 36px) - padding-left 8px + > .text + float right + width calc(100% - 36px) + padding-left 8px - p - margin 0 + p + margin 0 - i, .mk-reaction-icon - margin-right 4px + i, .mk-reaction-icon + margin-right 4px - .note-preview - color rgba(0, 0, 0, 0.7) + .note-preview + color isDark ? #c2cad4 : rgba(#000, 0.7) - .note-ref - color rgba(0, 0, 0, 0.7) + .note-ref + color isDark ? #c2cad4 : rgba(#000, 0.7) - [data-fa] - font-size 1em - font-weight normal - font-style normal - display inline-block - margin-right 3px + [data-fa] + font-size 1em + font-weight normal + font-style normal + display inline-block + margin-right 3px - &.renote, &.quote - .text p i - color #77B255 + &.renote, &.quote + .text p i + color #77B255 - &.follow - .text p i - color #53c7ce + &.follow + .text p i + color #53c7ce - &.reply, &.mention - .text p i - color #555 + &.reply, &.mention + .text p i + color #555 - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.8em - color #aaa - background #fdfdfd - border-bottom solid 1px rgba(0, 0, 0, 0.05) + > .date + display block + margin 0 + line-height 32px + text-align center + font-size 0.8em + color isDark ? #666b79 : #aaa + background isDark ? #242731 : #fdfdfd + border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05) - span - margin 0 16px + span + margin 0 16px - [data-fa] - margin-right 8px + [data-fa] + margin-right 8px > .more display block width 100% padding 16px color #555 - border-top solid 1px rgba(0, 0, 0, 0.05) + border-top solid 1px rgba(#000, 0.05) &:hover - background rgba(0, 0, 0, 0.025) + background rgba(#000, 0.025) &:active - background rgba(0, 0, 0, 0.05) + background rgba(#000, 0.05) &.fetching cursor wait @@ -312,4 +305,10 @@ export default Vue.extend({ > [data-fa] margin-right 4px +.mk-notifications[data-darkmode] + root(true) + +.mk-notifications:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index ebb0193088..984fc9866c 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -6,6 +6,11 @@ @drop.stop="onDrop" > <div class="content"> + <div v-if="visibility == 'specified'" class="visibleUsers"> + <span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span> + <a @click="addVisibleUser">+ユーザーを追加</a> + </div> + <input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)"> <textarea :class="{ with: (files.length != 0 || poll) }" ref="text" v-model="text" :disabled="posting" @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" @@ -27,8 +32,10 @@ <button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button> <button class="kao" title="%i18n:@insert-a-kao%" @click="kao">%fa:R smile%</button> <button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button> + <button class="poll" title="内容を隠す" @click="useCw = !useCw">%fa:eye-slash%</button> <button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> - <p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:!@text-remain%'.replace('{}', 1000 - text.length) }}</p> + <button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</button> + <p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p> <button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> {{ posting ? '%i18n:!@posting%' : submitText }}<mk-ellipsis v-if="posting"/> </button> @@ -41,12 +48,16 @@ import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; import getKao from '../../../common/scripts/get-kao'; +import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; export default Vue.extend({ components: { - XDraggable + XDraggable, + MkVisibilityChooser }, + props: ['reply', 'renote'], + data() { return { posting: false, @@ -54,11 +65,16 @@ export default Vue.extend({ files: [], uploadings: [], poll: false, + useCw: false, + cw: null, geo: null, + visibility: 'public', + visibleUsers: [], autocomplete: null, draghover: false }; }, + computed: { draftId(): string { return this.renote @@ -67,6 +83,7 @@ export default Vue.extend({ ? 'reply:' + this.reply.id : 'note'; }, + placeholder(): string { return this.renote ? '%i18n:!@quote-placeholder%' @@ -74,6 +91,7 @@ export default Vue.extend({ ? '%i18n:!@reply-placeholder%' : '%i18n:!@note-placeholder%'; }, + submitText(): string { return this.renote ? '%i18n:!@renote%' @@ -81,22 +99,17 @@ export default Vue.extend({ ? '%i18n:!@reply%' : '%i18n:!@note%'; }, + canPost(): boolean { return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote); } }, - watch: { - text() { - this.saveDraft(); - }, - poll() { - this.saveDraft(); - }, - files() { - this.saveDraft(); - } - }, + mounted() { + if (this.reply && this.reply.user.host != null) { + this.text = `@${this.reply.user.username}@${this.reply.user.host} `; + } + this.$nextTick(() => { // 書きかけの投稿を復元 const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId]; @@ -111,15 +124,26 @@ export default Vue.extend({ } this.$emit('change-attached-media', this.files); } + + this.$nextTick(() => this.watch()); }); }, + methods: { + watch() { + this.$watch('text', () => this.saveDraft()); + this.$watch('poll', () => this.saveDraft()); + this.$watch('files', () => this.saveDraft()); + }, + focus() { (this.$refs.text as any).focus(); }, + chooseFile() { (this.$refs.file as any).click(); }, + chooseFileFromDrive() { (this as any).apis.chooseDriveFile({ multiple: true @@ -127,32 +151,40 @@ export default Vue.extend({ files.forEach(this.attachMedia); }); }, + attachMedia(driveFile) { this.files.push(driveFile); this.$emit('change-attached-media', this.files); }, + detachMedia(id) { this.files = this.files.filter(x => x.id != id); this.$emit('change-attached-media', this.files); }, + onChangeFile() { Array.from((this.$refs.file as any).files).forEach(this.upload); }, + upload(file) { (this.$refs.uploader as any).upload(file); }, + onChangeUploadings(uploads) { this.$emit('change-uploadings', uploads); }, + clear() { this.text = ''; this.files = []; this.poll = false; this.$emit('change-attached-media', this.files); }, + onKeydown(e) { if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); }, + onPaste(e) { Array.from(e.clipboardData.items).forEach((item: any) => { if (item.kind == 'file') { @@ -160,6 +192,7 @@ export default Vue.extend({ } }); }, + onDragover(e) { const isFile = e.dataTransfer.items[0].kind == 'file'; const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; @@ -169,12 +202,15 @@ export default Vue.extend({ e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; } }, + onDragenter(e) { this.draghover = true; }, + onDragleave(e) { this.draghover = false; }, + onDrop(e): void { this.draghover = false; @@ -195,6 +231,7 @@ export default Vue.extend({ } //#endregion }, + setGeo() { if (navigator.geolocation == null) { alert('お使いの端末は位置情報に対応していません'); @@ -210,10 +247,38 @@ export default Vue.extend({ enableHighAccuracy: true }); }, + removeGeo() { this.geo = null; this.$emit('geo-dettached'); }, + + setVisibility() { + const w = (this as any).os.new(MkVisibilityChooser, { + source: this.$refs.visibilityButton, + v: this.visibility + }); + w.$once('chosen', v => { + this.visibility = v; + }); + }, + + addVisibleUser() { + (this as any).apis.input({ + title: 'ユーザー名を入力してください' + }).then(username => { + (this as any).api('users/show', { + username + }).then(user => { + this.visibleUsers.push(user); + }); + }); + }, + + removeVisibleUser(user) { + this.visibleUsers = this.visibleUsers.filter(u => u != user); + }, + post() { this.posting = true; @@ -223,6 +288,9 @@ export default Vue.extend({ replyId: this.reply ? this.reply.id : undefined, renoteId: this.renote ? this.renote.id : undefined, poll: this.poll ? (this.$refs.poll as any).get() : undefined, + cw: this.useCw ? this.cw || '' : undefined, + visibility: this.visibility, + visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, geo: this.geo ? { coordinates: [this.geo.longitude, this.geo.latitude], altitude: this.geo.altitude, @@ -250,6 +318,7 @@ export default Vue.extend({ this.posting = false; }); }, + saveDraft() { const data = JSON.parse(localStorage.getItem('drafts') || '{}'); @@ -264,6 +333,7 @@ export default Vue.extend({ localStorage.setItem('drafts', JSON.stringify(data)); }, + deleteDraft() { const data = JSON.parse(localStorage.getItem('drafts') || '{}'); @@ -271,6 +341,7 @@ export default Vue.extend({ localStorage.setItem('drafts', JSON.stringify(data)); }, + kao() { this.text += getKao(); } @@ -281,10 +352,10 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-post-form +root(isDark) display block padding 16px - background lighten($theme-color, 95%) + background isDark ? #282C37 : lighten($theme-color, 95%) &:after content "" @@ -292,56 +363,70 @@ export default Vue.extend({ clear both > .content - - textarea + > input + > textarea display block - padding 12px - margin 0 width 100% - max-width 100% - min-width 100% - min-height calc(16px + 12px + 12px) + padding 12px font-size 16px - color #333 - background #fff + color isDark ? #fff : #333 + background isDark ? #191d23 : #fff outline none border solid 1px rgba($theme-color, 0.1) border-radius 4px - transition border-color .3s ease + transition border-color .2s ease &:hover border-color rgba($theme-color, 0.2) transition border-color .1s ease + &:focus + border-color rgba($theme-color, 0.5) + transition border-color 0s ease + + &:disabled + opacity 0.5 + + &::-webkit-input-placeholder + color rgba($theme-color, 0.3) + + > input + margin-bottom 8px + + > textarea + margin 0 + max-width 100% + min-width 100% + min-height 64px + + &:hover & + * & + * + * border-color rgba($theme-color, 0.2) transition border-color .1s ease &:focus - color $theme-color - border-color rgba($theme-color, 0.5) - transition border-color 0s ease - & + * & + * + * border-color rgba($theme-color, 0.5) transition border-color 0s ease - &:disabled - opacity 0.5 - - &::-webkit-input-placeholder - color rgba($theme-color, 0.3) - &.with border-bottom solid 1px rgba($theme-color, 0.1) !important border-radius 4px 4px 0 0 + > .visibleUsers + margin-bottom 8px + font-size 14px + + > span + margin-right 16px + color isDark ? #fff : #666 + > .medias margin 0 padding 0 - background lighten($theme-color, 98%) + background isDark ? #181b23 : lighten($theme-color, 98%) border solid 1px rgba($theme-color, 0.1) border-top none border-radius 0 0 4px 4px @@ -392,7 +477,7 @@ export default Vue.extend({ cursor pointer > .mk-poll-editor - background lighten($theme-color, 98%) + background isDark ? #181b23 : lighten($theme-color, 98%) border solid 1px rgba($theme-color, 0.1) border-top none border-radius 0 0 4px 4px @@ -407,19 +492,6 @@ export default Vue.extend({ input[type='file'] display none - .text-count - pointer-events none - display block - position absolute - bottom 16px - right 138px - margin 0 - line-height 40px - color rgba($theme-color, 0.5) - - &.over - color #ec3828 - .submit display block position absolute @@ -484,11 +556,25 @@ export default Vue.extend({ from {background-position: 0 0;} to {background-position: -64px 32px;} + > .text-count + pointer-events none + display block + position absolute + bottom 16px + right 138px + margin 0 + line-height 40px + color rgba($theme-color, 0.5) + + &.over + color #ec3828 + > .upload > .drive > .kao > .poll > .geo + > .visibility display inline-block cursor pointer padding 0 @@ -496,7 +582,7 @@ export default Vue.extend({ width 40px height 40px font-size 1em - color rgba($theme-color, 0.5) + color isDark ? $theme-color : rgba($theme-color, 0.5) background transparent outline none border solid 1px transparent @@ -504,13 +590,13 @@ export default Vue.extend({ &:hover background transparent - border-color rgba($theme-color, 0.3) + border-color isDark ? rgba($theme-color, 0.5) : rgba($theme-color, 0.3) &:active color rgba($theme-color, 0.6) - background linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%) + background isDark ? transparent : linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%) border-color rgba($theme-color, 0.5) - box-shadow 0 2px 4px rgba(0, 0, 0, 0.15) inset + box-shadow 0 2px 4px rgba(#000, 0.15) inset &:focus &:after @@ -533,4 +619,10 @@ export default Vue.extend({ border dashed 2px rgba($theme-color, 0.5) pointer-events none +.mk-post-form[data-darkmode] + root(true) + +.mk-post-form:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue index daae5df5e9..9c0154211b 100644 --- a/src/client/app/desktop/views/components/renote-form.vue +++ b/src/client/app/desktop/views/components/renote-form.vue @@ -4,8 +4,8 @@ <template v-if="!quote"> <footer> <a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a> - <button class="cancel" @click="cancel">%i18n:@cancel%</button> - <button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!@reposting%' : '%i18n:!@renote%' }}</button> + <button class="ui cancel" @click="cancel">%i18n:@cancel%</button> + <button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!@reposting%' : '%i18n:!@renote%' }}</button> </footer> </template> <template v-if="quote"> @@ -59,14 +59,14 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-renote-form +root(isDark) > .mk-note-preview margin 16px 22px > footer height 72px - background lighten($theme-color, 95%) + background isDark ? #313543 : lighten($theme-color, 95%) > .quote position absolute @@ -78,54 +78,19 @@ export default Vue.extend({ display block position absolute bottom 16px - cursor pointer - padding 0 - margin 0 width 120px height 40px - font-size 1em - outline none - border-radius 4px - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px + &.cancel + right 148px - > .cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 + &.ok + right 16px - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc +.mk-renote-form[data-darkmode] + root(true) - &:active - background #ececec - border-color #dcdcdc - - > .ok - right 16px - font-weight bold - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:hover - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active - background $theme-color - border-color $theme-color +.mk-renote-form:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/components/repost-form.vue b/src/client/app/desktop/views/components/repost-form.vue deleted file mode 100644 index d5b1696757..0000000000 --- a/src/client/app/desktop/views/components/repost-form.vue +++ /dev/null @@ -1,131 +0,0 @@ -<template> -<div class="mk-renote-form"> - <mk-note-preview :note="note"/> - <template v-if="!quote"> - <footer> - <a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-renote-form.quote%</a> - <button class="cancel" @click="cancel">%i18n:desktop.tags.mk-renote-form.cancel%</button> - <button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!desktop.tags.mk-renote-form.reposting%' : '%i18n:!desktop.tags.mk-renote-form.renote%' }}</button> - </footer> - </template> - <template v-if="quote"> - <mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/> - </template> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['note'], - data() { - return { - wait: false, - quote: false - }; - }, - methods: { - ok() { - this.wait = true; - (this as any).api('notes/create', { - renoteId: this.note.id - }).then(data => { - this.$emit('posted'); - (this as any).apis.notify('%i18n:!desktop.tags.mk-renote-form.success%'); - }).catch(err => { - (this as any).apis.notify('%i18n:!desktop.tags.mk-renote-form.failure%'); - }).then(() => { - this.wait = false; - }); - }, - cancel() { - this.$emit('canceled'); - }, - onQuote() { - this.quote = true; - - this.$nextTick(() => { - (this.$refs.form as any).focus(); - }); - }, - onChildFormPosted() { - this.$emit('posted'); - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-renote-form - - > .mk-note-preview - margin 16px 22px - - > footer - height 72px - background lighten($theme-color, 95%) - - > .quote - position absolute - bottom 16px - left 28px - line-height 40px - - button - display block - position absolute - bottom 16px - cursor pointer - padding 0 - margin 0 - width 120px - height 40px - font-size 1em - outline none - border-radius 4px - - &:focus - &:after - content "" - pointer-events none - position absolute - top -5px - right -5px - bottom -5px - left -5px - border 2px solid rgba($theme-color, 0.3) - border-radius 8px - - > .cancel - right 148px - color #888 - background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%) - border solid 1px #e2e2e2 - - &:hover - background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%) - border-color #dcdcdc - - &:active - background #ececec - border-color #dcdcdc - - > .ok - right 16px - font-weight bold - color $theme-color-foreground - background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%) - border solid 1px lighten($theme-color, 15%) - - &:hover - background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%) - border-color $theme-color - - &:active - background $theme-color - border-color $theme-color - -</style> diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue index a43c6e8ea6..377f2e689b 100644 --- a/src/client/app/desktop/views/components/settings.api.vue +++ b/src/client/app/desktop/views/components/settings.api.vue @@ -29,8 +29,6 @@ export default Vue.extend({ <style lang="stylus" scoped> .root.api - color #4a535a - code display inline-block padding 4px 6px diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 3d88ccb6c2..9439ded2fc 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -20,7 +20,7 @@ <section class="web" v-show="page == 'web'"> <h1>動作</h1> - <mk-switch v-model="os.i.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み"> + <mk-switch v-model="clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み"> <span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span> </mk-switch> <mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト"> @@ -37,13 +37,20 @@ <section class="web" v-show="page == 'web'"> <h1>デザインと表示</h1> <div class="div"> - <button class="ui button" @click="customizeHome">ホームをカスタマイズ</button> + <button class="ui button" @click="customizeHome" style="margin-bottom: 16px">ホームをカスタマイズ</button> </div> - <mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/> - <mk-switch v-model="os.i.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開"> + <div class="div"> + <mk-switch v-model="darkmode" text="ダークモード"/> + <mk-switch v-model="clientSettings.circleIcons" @change="onChangeCircleIcons" text="円形のアイコンを使用"/> + <mk-switch v-model="clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/> + </div> + <mk-switch v-model="clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/> + <mk-switch v-model="clientSettings.showReplyTarget" @change="onChangeShowReplyTarget" text="リプライ先を表示する"/> + <mk-switch v-model="clientSettings.showMyRenotes" @change="onChangeShowMyRenotes" text="自分の行ったRenoteをタイムラインに表示する"/> + <mk-switch v-model="clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="Renoteされた自分の投稿をタイムラインに表示する"/> + <mk-switch v-model="clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開"> <span>位置情報が添付された投稿のマップを自動的に展開します。</span> </mk-switch> - <mk-switch v-model="os.i.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/> </section> <section class="web" v-show="page == 'web'"> @@ -63,7 +70,7 @@ <section class="web" v-show="page == 'web'"> <h1>モバイル</h1> - <mk-switch v-model="os.i.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/> + <mk-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/> </section> <section class="web" v-show="page == 'web'"> @@ -76,6 +83,7 @@ <el-option label="ja-JP" value="ja"/> <el-option label="en-US" value="en"/> <el-option label="fr" value="fr"/> + <el-option label="pl" value="pl"/> </el-option-group> </el-select> <div class="none ui info"> @@ -228,6 +236,7 @@ export default Vue.extend({ version, latestVersion: undefined, checkingForUpdate: false, + darkmode: localStorage.getItem('darkmode') == 'true', enableSounds: localStorage.getItem('enableSounds') == 'true', autoPopout: localStorage.getItem('autoPopout') == 'true', apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true, @@ -251,6 +260,9 @@ export default Vue.extend({ apiViaStream() { localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false'); }, + darkmode() { + (this as any)._updateDarkmode_(this.darkmode); + }, enableSounds() { localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false'); }, @@ -287,8 +299,8 @@ export default Vue.extend({ this.$emit('done'); }, onChangeFetchOnScroll(v) { - (this as any).api('i/update_client_setting', { - name: 'fetchOnScroll', + this.$store.dispatch('settings/set', { + key: 'fetchOnScroll', value: v }); }, @@ -297,27 +309,57 @@ export default Vue.extend({ autoWatch: v }); }, + onChangeDark(v) { + this.$store.dispatch('settings/set', { + key: 'dark', + value: v + }); + }, onChangeShowPostFormOnTopOfTl(v) { - (this as any).api('i/update_client_setting', { - name: 'showPostFormOnTopOfTl', + this.$store.dispatch('settings/set', { + key: 'showPostFormOnTopOfTl', + value: v + }); + }, + onChangeShowReplyTarget(v) { + this.$store.dispatch('settings/set', { + key: 'showReplyTarget', + value: v + }); + }, + onChangeShowMyRenotes(v) { + this.$store.dispatch('settings/set', { + key: 'showMyRenotes', + value: v + }); + }, + onChangeShowRenotedMyNotes(v) { + this.$store.dispatch('settings/set', { + key: 'showRenotedMyNotes', value: v }); }, onChangeShowMaps(v) { - (this as any).api('i/update_client_setting', { - name: 'showMaps', + this.$store.dispatch('settings/set', { + key: 'showMaps', + value: v + }); + }, + onChangeCircleIcons(v) { + this.$store.dispatch('settings/set', { + key: 'circleIcons', value: v }); }, onChangeGradientWindowHeader(v) { - (this as any).api('i/update_client_setting', { - name: 'gradientWindowHeader', + this.$store.dispatch('settings/set', { + key: 'gradientWindowHeader', value: v }); }, onChangeDisableViaMobile(v) { - (this as any).api('i/update_client_setting', { - name: 'disableViaMobile', + this.$store.dispatch('settings/set', { + key: 'disableViaMobile', value: v }); }, @@ -358,7 +400,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-settings +root(isDark) display flex width 100% height 100% @@ -369,13 +411,13 @@ export default Vue.extend({ height 100% padding 16px 0 0 0 overflow auto - border-right solid 1px #ddd + border-right solid 1px isDark ? #1c2023 : #ddd > p display block padding 10px 16px margin 0 - color #666 + color isDark ? #9aa2a7 : #666 cursor pointer user-select none transition margin-left 0.2s ease @@ -384,7 +426,7 @@ export default Vue.extend({ margin-right 4px &:hover - color #555 + color isDark ? #fff : #555 &.active margin-left 8px @@ -398,14 +440,14 @@ export default Vue.extend({ > section margin 32px - color #4a535a + color isDark ? #c4ccd2 : #4a535a > h1 margin 0 0 1em 0 padding 0 0 8px 0 font-size 1em - color #555 - border-bottom solid 1px #eee + color isDark ? #e3e7ea : #555 + border-bottom solid 1px isDark ? #1c2023 : #eee &, >>> * .ui.button.block @@ -418,13 +460,18 @@ export default Vue.extend({ margin 0 0 1em 0 padding 0 0 8px 0 font-size 1em - color #555 - border-bottom solid 1px #eee + color isDark ? #e3e7ea : #555 + border-bottom solid 1px isDark ? #1c2023 : #eee > .web > .div - border-bottom solid 1px #eee - padding 0 0 16px 0 - margin 0 0 16px 0 + border-bottom solid 1px isDark ? #1c2023 : #eee + margin 16px 0 + +.mk-settings[data-darkmode] + root(true) + +.mk-settings:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue index 51ee93cba6..dd4012039b 100644 --- a/src/client/app/desktop/views/components/sub-note-content.vue +++ b/src/client/app/desktop/views/components/sub-note-content.vue @@ -1,6 +1,7 @@ <template> <div class="mk-sub-note-content"> <div class="body"> + <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span> <a class="reply" v-if="note.replyId">%fa:reply%</a> <mk-note-html :text="note.text" :i="os.i"/> <a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a> diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index 1e98f087e1..254a5b9d63 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -1,24 +1,23 @@ <template> -<div class="mk-home-timeline"> +<div class="mk-timeline-core"> <mk-friends-maker v-if="src == 'home' && alone"/> <div class="fetching" v-if="fetching"> <mk-ellipsis-icon/> </div> - <p class="empty" v-if="notes.length == 0 && !fetching"> - %fa:R comments%%i18n:@empty% - </p> - <mk-notes :notes="notes" ref="timeline"> - <button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">%i18n:@load-more%</template> - <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> - </button> + + <mk-notes ref="timeline" :more="canFetchMore ? more : null"> + <p :class="$style.empty" slot="empty"> + %fa:R comments%%i18n:@empty% + </p> </mk-notes> </div> </template> <script lang="ts"> import Vue from 'vue'; -import { url } from '../../../config'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +const fetchLimit = 10; export default Vue.extend({ props: { @@ -33,9 +32,9 @@ export default Vue.extend({ fetching: true, moreFetching: false, existMore: false, - notes: [], connection: null, connectionId: null, + unreadCount: 0, date: null }; }, @@ -59,6 +58,10 @@ export default Vue.extend({ : this.src == 'local' ? 'notes/local-timeline' : 'notes/global-timeline'; + }, + + canFetchMore(): boolean { + return !this.moreFetching && !this.fetching && this.existMore; } }, @@ -72,6 +75,9 @@ export default Vue.extend({ this.connection.on('unfollow', this.onChangeFollowing); } + document.addEventListener('keydown', this.onKeydown); + document.addEventListener('visibilitychange', this.onVisibilitychange, false); + this.fetch(); }, @@ -82,56 +88,62 @@ export default Vue.extend({ this.connection.off('unfollow', this.onChangeFollowing); } this.stream.dispose(this.connectionId); + + document.removeEventListener('keydown', this.onKeydown); + document.removeEventListener('visibilitychange', this.onVisibilitychange); }, methods: { - fetch(cb?) { + fetch() { this.fetching = true; - (this as any).api(this.endpoint, { - limit: 11, - untilDate: this.date ? this.date.getTime() : undefined - }).then(notes => { - if (notes.length == 11) { - notes.pop(); - this.existMore = true; - } - this.notes = notes; - this.fetching = false; - this.$emit('loaded'); - if (cb) cb(); - }); + (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).clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).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.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return; + if (!this.canFetchMore) return; + this.moreFetching = true; + (this as any).api(this.endpoint, { - limit: 11, - untilId: this.notes[this.notes.length - 1].id + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: (this as any).clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes }).then(notes => { - if (notes.length == 11) { + if (notes.length == fetchLimit + 1) { notes.pop(); } else { this.existMore = false; } - this.notes = this.notes.concat(notes); + notes.forEach(n => (this.$refs.timeline as any).append(n)); this.moreFetching = false; }); }, onNote(note) { - // サウンドを再生する - if ((this as any).os.isEnableSounds) { - const sound = new Audio(`${url}/assets/post.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; - sound.play(); + if (document.hidden && note.userId !== (this as any).os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; } - this.notes.unshift(note); - - const isTop = window.scrollY > 8; - if (isTop) this.notes.pop(); + // Prepend a note + (this.$refs.timeline as any).prepend(note); }, onChangeFollowing() { @@ -145,31 +157,51 @@ export default Vue.extend({ warp(date) { this.date = date; 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> <style lang="stylus" scoped> -.mk-home-timeline +@import '~const.styl' + +.mk-timeline-core > .mk-friends-maker border-bottom solid 1px #eee > .fetching padding 64px 0 - > .empty - display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 +</style> + +<style lang="stylus" module> +.empty + display block + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 - > [data-fa] - display block - margin-bottom 16px - font-size 3em - color #ccc + > [data-fa] + display block + margin-bottom 16px + font-size 3em + color #ccc </style> diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index e0215ad1a2..a776e40a24 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -1,19 +1,23 @@ <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-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> + <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> + <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> + <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> + <button @click="chooseList" title="%i18n:@list%">%fa:list%</button> </header> <x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> <x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> <x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> + <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> </div> </template> <script lang="ts"> import Vue from 'vue'; import XCore from './timeline.core.vue'; +import MkUserListsWindow from './user-lists-window.vue'; export default Vue.extend({ components: { @@ -22,44 +26,35 @@ export default Vue.extend({ data() { return { - src: 'home' + src: 'home', + list: null }; }, - mounted() { - document.addEventListener('keydown', this.onKeydown); - window.addEventListener('scroll', this.onScroll); - - console.log(this.$refs.tl); + created() { + if ((this as any).os.i.followingCount == 0) { + this.src = 'local'; + } + }, + mounted() { (this.$refs.tl as any).$once('loaded', () => { this.$emit('loaded'); }); }, - beforeDestroy() { - document.removeEventListener('keydown', this.onKeydown); - window.removeEventListener('scroll', this.onScroll); - }, - methods: { - onScroll() { - if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) (this.$refs.tl as any).more(); - } - }, - - onKeydown(e) { - if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') { - if (e.which == 84) { // t - (this.$refs.tl as any).focus(); - } - } - }, - warp(date) { (this.$refs.tl as any).warp(date); + }, + + chooseList() { + const w = (this as any).os.new(MkUserListsWindow); + w.$once('choosen', list => { + this.list = list; + this.src = 'list'; + w.close(); + }); } } }); @@ -68,26 +63,68 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-timeline - background #fff - border solid 1px rgba(0, 0, 0, 0.075) +root(isDark) + background isDark ? #282C37 : #fff + border solid 1px rgba(#000, 0.075) border-radius 6px > header - padding 8px 16px - border-bottom solid 1px #eee + padding 0 8px + z-index 10 + background isDark ? #313543 : #fff + border-radius 6px 6px 0 0 + box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08) + + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color isDark ? #9baec8 : #ccc + + &:hover + color isDark ? #b2c1d5 : #aaa + + &:active + color isDark ? #b2c1d5 : #999 > span - margin-right 16px - line-height 27px - font-size 14px - color #555 + display inline-block + padding 0 10px + line-height 42px + font-size 12px + user-select none - &:not([data-is-active]) + &[data-active] color $theme-color + cursor default + font-weight bold + + &:before + content "" + display block + position absolute + bottom 0 + left -8px + width calc(100% + 16px) + height 2px + background $theme-color + + &:not([data-active]) + color isDark ? #9aa2a7 : #6f7477 cursor pointer &:hover - text-decoration underline + color isDark ? #d9dcde : #525a5f + +.mk-timeline[data-darkmode] + root(true) + +.mk-timeline:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue index 558aaa6dc8..fd15ea6006 100644 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -2,32 +2,40 @@ <div class="account"> <button class="header" :data-active="isOpen" @click="toggle"> <span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span> - <img class="avatar" :src="`${ os.i.avatarUrl }?thumbnail&size=64`" alt="avatar"/> + <mk-avatar class="avatar" :user="os.i"/> </button> <transition name="zoom-in-top"> <div class="menu" v-if="isOpen"> <ul> <li> - <router-link :to="`/@${ os.i.username }`">%fa:user%%i18n:@profile%%fa:angle-right%</router-link> + <router-link :to="`/@${ os.i.username }`">%fa:user%<span>%i18n:@profile%</span>%fa:angle-right%</router-link> </li> <li @click="drive"> - <p>%fa:cloud%%i18n:@drive%%fa:angle-right%</p> + <p>%fa:cloud%<span>%i18n:@drive%</span>%fa:angle-right%</p> </li> <li> - <a href="/i/mentions">%fa:at%%i18n:@mentions%%fa:angle-right%</a> + <router-link to="/i/favorites">%fa:star%<span>%i18n:@favorites%</span>%fa:angle-right%</router-link> + </li> + <li @click="list"> + <p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p> </li> </ul> <ul> <li> - <a href="/i/customize-home">%fa:wrench%%i18n:@customize%%fa:angle-right%</a> + <router-link to="/i/customize-home">%fa:wrench%<span>%i18n:@customize%</span>%fa:angle-right%</router-link> </li> <li @click="settings"> - <p>%fa:cog%%i18n:@settings%%fa:angle-right%</p> + <p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p> </li> </ul> <ul> <li @click="signout"> - <p>%fa:power-off%%i18n:@signout%%fa:angle-right%</p> + <p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p> + </li> + </ul> + <ul> + <li @click="dark"> + <p><span>%i18n:@dark%</span><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template></p> </li> </ul> </div> @@ -37,6 +45,7 @@ <script lang="ts"> import Vue from 'vue'; +import MkUserListsWindow from './user-lists-window.vue'; import MkSettingsWindow from './settings-window.vue'; import MkDriveWindow from './drive-window.vue'; import contains from '../../../common/scripts/contains'; @@ -75,12 +84,22 @@ export default Vue.extend({ this.close(); (this as any).os.new(MkDriveWindow); }, + list() { + this.close(); + const w = (this as any).os.new(MkUserListsWindow); + w.$once('choosen', list => { + this.$router.push(`i/lists/${ list.id }`); + }); + }, settings() { this.close(); (this as any).os.new(MkSettingsWindow); }, signout() { (this as any).os.signout(); + }, + dark() { + (this as any)._updateDarkmode_(!(this as any)._darkmode_); } } }); @@ -89,7 +108,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.account +root(isDark) > .header display block margin 0 @@ -104,13 +123,13 @@ export default Vue.extend({ &:hover &[data-active='true'] - color darken(#9eaba8, 20%) + color isDark ? #fff : darken(#9eaba8, 20%) > .avatar filter saturate(150%) &:active - color darken(#9eaba8, 30%) + color isDark ? #fff : darken(#9eaba8, 30%) > .username display block @@ -137,15 +156,16 @@ export default Vue.extend({ transition filter 100ms ease > .menu + $bgcolor = isDark ? #282c37 : #fff display block position absolute top 56px right -2px width 230px font-size 0.8em - background #fff + background $bgcolor border-radius 4px - box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) + box-shadow 0 1px 4px rgba(#000, 0.25) &:before content "" @@ -156,7 +176,7 @@ export default Vue.extend({ right 12px border-top solid 14px transparent border-right solid 14px transparent - border-bottom solid 14px rgba(0, 0, 0, 0.1) + border-bottom solid 14px rgba(#000, 0.1) border-left solid 14px transparent &:after @@ -168,7 +188,7 @@ export default Vue.extend({ right 12px border-top solid 14px transparent border-right solid 14px transparent - border-bottom solid 14px #fff + border-bottom solid 14px $bgcolor border-left solid 14px transparent ul @@ -179,7 +199,7 @@ export default Vue.extend({ & + ul padding-top 10px - border-top solid 1px #eee + border-top solid 1px isDark ? #1c2023 : #eee > li display block @@ -193,16 +213,20 @@ export default Vue.extend({ padding 0 28px margin 0 line-height 40px - color #868C8C + color isDark ? #c8cece : #868C8C cursor pointer * pointer-events none - > [data-fa]:first-of-type + > span:first-child + padding-left 22px + + > [data-fa]:first-child margin-right 6px + width 16px - > [data-fa]:last-of-type + > [data-fa]:last-child display block position absolute top 0 @@ -220,9 +244,25 @@ export default Vue.extend({ &:active background darken($theme-color, 10%) + &.signout + $color = #e64137 + + &:hover, &:active + background $color + color #fff + + &:active + background darken($color, 10%) + .zoom-in-top-enter-active, .zoom-in-top-leave-active { transform-origin: center -16px; } +.account[data-darkmode] + root(true) + +.account:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue index 19f72a86d7..0800d96eb6 100644 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ b/src/client/app/desktop/views/components/ui.header.nav.vue @@ -99,7 +99,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.nav +root(isDark) display inline-block margin 0 padding 0 @@ -131,7 +131,7 @@ export default Vue.extend({ padding 0 24px font-size 13px font-variant small-caps - color #9eaba8 + color isDark ? #b8c5ca : #9eaba8 text-decoration none transition none cursor pointer @@ -140,7 +140,7 @@ export default Vue.extend({ pointer-events none &:hover - color darken(#9eaba8, 20%) + color isDark ? #fff : darken(#9eaba8, 20%) text-decoration none > [data-fa]:first-child @@ -164,4 +164,10 @@ export default Vue.extend({ @media (max-width 700px) padding 0 12px +.nav[data-darkmode] + root(true) + +.nav:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue index e9a6b9b04f..ea814dd7a3 100644 --- a/src/client/app/desktop/views/components/ui.header.notifications.vue +++ b/src/client/app/desktop/views/components/ui.header.notifications.vue @@ -84,7 +84,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.notifications +root(isDark) > button display block @@ -101,10 +101,10 @@ export default Vue.extend({ &:hover &[data-active='true'] - color darken(#9eaba8, 20%) + color isDark ? #fff : darken(#9eaba8, 20%) &:active - color darken(#9eaba8, 30%) + color isDark ? #fff : darken(#9eaba8, 30%) > [data-fa].bell font-size 1.2em @@ -117,14 +117,15 @@ export default Vue.extend({ color $theme-color > .pop + $bgcolor = isDark ? #282c37 : #fff display block position absolute top 56px right -72px width 300px - background #fff + background $bgcolor border-radius 4px - box-shadow 0 1px 4px rgba(0, 0, 0, 0.25) + box-shadow 0 1px 4px rgba(#000, 0.25) &:before content "" @@ -135,7 +136,7 @@ export default Vue.extend({ right 74px border-top solid 14px transparent border-right solid 14px transparent - border-bottom solid 14px rgba(0, 0, 0, 0.1) + border-bottom solid 14px rgba(#000, 0.1) border-left solid 14px transparent &:after @@ -147,7 +148,7 @@ export default Vue.extend({ right 74px border-top solid 14px transparent border-right solid 14px transparent - border-bottom solid 14px #fff + border-bottom solid 14px $bgcolor border-left solid 14px transparent > .mk-notifications @@ -155,4 +156,10 @@ export default Vue.extend({ font-size 1rem overflow auto +.notifications[data-darkmode] + root(true) + +.notifications:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue index 3167aab8ab..1ed28ba3a8 100644 --- a/src/client/app/desktop/views/components/ui.header.search.vue +++ b/src/client/app/desktop/views/components/ui.header.search.vue @@ -50,7 +50,7 @@ export default Vue.extend({ width 14em height 32px font-size 1em - background rgba(0, 0, 0, 0.05) + background rgba(#000, 0.05) outline none //border solid 1px #ddd border none @@ -62,7 +62,7 @@ export default Vue.extend({ color #9eaba8 &:hover - background rgba(0, 0, 0, 0.08) + background rgba(#000, 0.08) &:focus box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index 2b63030cd2..7729575b56 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -43,10 +43,13 @@ export default Vue.extend({ XClock, }, mounted() { + this.$store.commit('setUiHeaderHeight', 48); + if ((this as any).os.isSignedIn) { - const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000 + const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000; const isHisasiburi = ago >= 3600; (this as any).os.i.lastUsedAt = new Date(); + (this as any).os.bakeMe(); if (isHisasiburi) { (this.$refs.welcomeback as any).style.display = 'block'; (this.$refs.main as any).style.overflow = 'hidden'; @@ -101,7 +104,7 @@ root(isDark) top 0 z-index 1000 width 100% - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) + box-shadow 0 1px 1px rgba(#000, 0.075) > .main height 48px @@ -130,7 +133,7 @@ root(isDark) line-height 48px margin 0 text-align center - color #888 + color isDark ? #fff : #888 opacity 0 > .container @@ -169,10 +172,10 @@ root(isDark) > .mk-ui-header-search display none -.header[data-is-darkmode] +.header[data-darkmode] root(true) -.header +.header:not([data-darkmode]) root(false) </style> diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue new file mode 100644 index 0000000000..59d6abbbc1 --- /dev/null +++ b/src/client/app/desktop/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).clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).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/user-list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: (this as any).clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).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/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue new file mode 100644 index 0000000000..d082610132 --- /dev/null +++ b/src/client/app/desktop/views/components/user-lists-window.vue @@ -0,0 +1,69 @@ +<template> +<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> + <span slot="header">%fa:list% リスト</span> + + <div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_"> + <button class="ui" @click="add">リストを作成</button> + <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a> + </div> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + lists: [] + }; + }, + mounted() { + (this as any).api('users/lists/list').then(lists => { + this.fetching = false; + this.lists = lists; + }); + }, + methods: { + add() { + (this as any).apis.input({ + title: 'リスト名', + }).then(async title => { + const list = await (this as any).api('users/lists/create', { + title + }); + + this.$emit('choosen', list); + }); + }, + choice(list) { + this.$emit('choosen', list); + }, + close() { + (this as any).$refs.window.close(); + } + } +}); +</script> + +<style lang="stylus" scoped> + +root(isDark) + padding 16px + + > button + margin-bottom 16px + + > a + display block + padding 16px + border solid 1px isDark ? #1c2023 : #eee + border-radius 4px + +[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"][data-darkmode] + root(true) + +[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"]:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue index bcd79dc2af..cc5e021390 100644 --- a/src/client/app/desktop/views/components/user-preview.vue +++ b/src/client/app/desktop/views/components/user-preview.vue @@ -2,11 +2,9 @@ <div class="mk-user-preview"> <template v-if="u != null"> <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div> - <router-link class="avatar" :to="u | userPage"> - <img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" :user="u" :disable-preview="true"/> <div class="title"> - <router-link class="name" :to="u | userPage">{{ u.name }}</router-link> + <router-link class="name" :to="u | userPage">{{ u | userName }}</router-link> <p class="username">@{{ u | acct }}</p> </div> <div class="description">{{ u.description }}</div> @@ -87,21 +85,21 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-user-preview +root(isDark) position absolute z-index 2048 margin-top -8px width 250px - background #fff + background isDark ? #282c37 : #fff background-clip content-box - border solid 1px rgba(0, 0, 0, 0.1) + border solid 1px rgba(#000, 0.1) border-radius 4px overflow hidden opacity 0 > .banner height 84px - background-color #f5f5f5 + background-color isDark ? #1c1e26 : #f5f5f5 background-size cover background-position center @@ -111,14 +109,10 @@ export default Vue.extend({ top 62px left 13px z-index 2 - - > img - display block - width 58px - height 58px - margin 0 - border solid 3px #fff - border-radius 8px + width 58px + height 58px + border solid 3px isDark ? #282c37 : #fff + border-radius 8px > .title display block @@ -129,19 +123,19 @@ export default Vue.extend({ margin 0 font-weight bold line-height 16px - color #656565 + color isDark ? #fff : #656565 > .username display block margin 0 line-height 16px font-size 0.8em - color #999 + color isDark ? #606984 : #999 > .description padding 0 16px font-size 0.7em - color #555 + color isDark ? #9ea4ad : #555 > .status padding 8px 16px @@ -164,4 +158,10 @@ export default Vue.extend({ top 92px right 8px +.mk-user-preview[data-darkmode] + root(true) + +.mk-user-preview:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue index 005c9cd6d3..dbad295178 100644 --- a/src/client/app/desktop/views/components/users-list.item.vue +++ b/src/client/app/desktop/views/components/users-list.item.vue @@ -1,8 +1,6 @@ <template> <div class="root item"> - <router-link class="avatar-anchor" :to="user | userPage" v-user-preview="user.id"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" :user="user"/> <div class="main"> <header> <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> @@ -35,18 +33,13 @@ export default Vue.extend({ display block clear both - > .avatar-anchor + > .avatar display block float left margin 0 16px 0 0 - - > .avatar - display block - width 58px - height 58px - margin 0 - border-radius 8px - vertical-align bottom + width 58px + height 58px + border-radius 8px > .main float left diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue index a08e76f573..13d0d07bbc 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 @@ -119,7 +119,7 @@ export default Vue.extend({ overflow auto > * - border-bottom solid 1px rgba(0, 0, 0, 0.05) + border-bottom solid 1px rgba(#000, 0.05) > * max-width 600px diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue index 188a67313e..ab8327d39e 100644 --- a/src/client/app/desktop/views/components/widget-container.vue +++ b/src/client/app/desktop/views/components/widget-container.vue @@ -24,8 +24,8 @@ export default Vue.extend({ computed: { withGradient(): boolean { return (this as any).os.isSignedIn - ? (this as any).os.i.clientSettings.gradientWindowHeader != null - ? (this as any).os.i.clientSettings.gradientWindowHeader + ? (this as any).clientSettings.gradientWindowHeader != null + ? (this as any).clientSettings.gradientWindowHeader : false : false; } @@ -34,9 +34,9 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-widget-container - background #fff - border solid 1px rgba(0, 0, 0, 0.075) +root(isDark) + background isDark ? #282C37 : #fff + border solid 1px rgba(#000, 0.075) border-radius 6px overflow hidden @@ -45,6 +45,8 @@ export default Vue.extend({ border none !important > header + background isDark ? #313543 : #fff + > .title z-index 1 margin 0 @@ -52,11 +54,11 @@ export default Vue.extend({ line-height 42px font-size 0.9em font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) + color isDark ? #e3e5e8 : #888 + box-shadow 0 1px rgba(#000, 0.07) > [data-fa] - margin-right 4px + margin-right 6px &:empty display none @@ -70,16 +72,23 @@ export default Vue.extend({ width 42px font-size 0.9em line-height 42px - color #ccc + color isDark ? #9baec8 : #ccc &:hover - color #aaa + color isDark ? #b2c1d5 : #aaa &:active - color #999 + color isDark ? #b2c1d5 : #999 &.withGradient > .title - background linear-gradient(to bottom, #fff, #ececec) + background isDark ? linear-gradient(to bottom, #313543, #1d2027) : linear-gradient(to bottom, #fff, #ececec) box-shadow 0 1px rgba(#000, 0.11) + +.mk-widget-container[data-darkmode] + root(true) + +.mk-widget-container:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue index e2cab21799..2e7eb557b4 100644 --- a/src/client/app/desktop/views/components/window.vue +++ b/src/client/app/desktop/views/components/window.vue @@ -4,7 +4,7 @@ <div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }"> <div class="body"> <header ref="header" - :class="{ withGradient }" + :class="{ withGradient: clientSettings.gradientWindowHeader }" @contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown" > <h1><slot name="header"></slot></h1> @@ -17,14 +17,16 @@ <slot></slot> </div> </div> - <div class="handle top" v-if="canResize" @mousedown.prevent="onTopHandleMousedown"></div> - <div class="handle right" v-if="canResize" @mousedown.prevent="onRightHandleMousedown"></div> - <div class="handle bottom" v-if="canResize" @mousedown.prevent="onBottomHandleMousedown"></div> - <div class="handle left" v-if="canResize" @mousedown.prevent="onLeftHandleMousedown"></div> - <div class="handle top-left" v-if="canResize" @mousedown.prevent="onTopLeftHandleMousedown"></div> - <div class="handle top-right" v-if="canResize" @mousedown.prevent="onTopRightHandleMousedown"></div> - <div class="handle bottom-right" v-if="canResize" @mousedown.prevent="onBottomRightHandleMousedown"></div> - <div class="handle bottom-left" v-if="canResize" @mousedown.prevent="onBottomLeftHandleMousedown"></div> + <template v-if="canResize"> + <div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div> + <div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div> + <div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div> + <div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div> + <div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div> + <div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div> + <div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div> + <div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div> + </template> </div> </div> </template> @@ -85,17 +87,10 @@ export default Vue.extend({ computed: { isFlexible(): boolean { - return this.height == null; + return this.height == 'auto'; }, canResize(): boolean { return !this.isFlexible; - }, - withGradient(): boolean { - return (this as any).os.isSignedIn - ? (this as any).os.i.clientSettings.gradientWindowHeader != null - ? (this as any).os.i.clientSettings.gradientWindowHeader - : false - : false; } }, @@ -465,7 +460,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-window +root(isDark) display block > .bg @@ -476,7 +471,7 @@ export default Vue.extend({ left 0 width 100% height 100% - background rgba(0, 0, 0, 0.7) + background rgba(#000, 0.7) opacity 0 pointer-events none @@ -493,7 +488,7 @@ export default Vue.extend({ &:focus &:not([data-is-modal]) > .body - box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(0, 0, 0, 0.2) + box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(#000, 0.2) > .handle $size = 8px @@ -559,9 +554,9 @@ export default Vue.extend({ > .body height 100% overflow hidden - background #fff + background isDark ? #282C37 : #fff border-radius 6px - box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2) + box-shadow 0 2px 6px 0 rgba(#000, 0.2) > header $header-height = 40px @@ -571,12 +566,12 @@ export default Vue.extend({ overflow hidden white-space nowrap cursor move - background #fff + background isDark ? #313543 : #fff border-radius 6px 6px 0 0 box-shadow 0 1px 0 rgba(#000, 0.1) &.withGradient - background linear-gradient(to bottom, #fff, #ececec) + background isDark ? linear-gradient(to bottom, #313543, #1d2027) : linear-gradient(to bottom, #fff, #ececec) box-shadow 0 1px 0 rgba(#000, 0.15) &, * @@ -593,7 +588,7 @@ export default Vue.extend({ font-size 1em line-height $header-height font-weight normal - color #666 + color isDark ? #e3e5e8 : #666 > div:last-child position absolute @@ -608,16 +603,16 @@ export default Vue.extend({ padding 0 cursor pointer font-size 1em - color rgba(#000, 0.4) + color isDark ? #9baec8 : rgba(#000, 0.4) border none outline none background transparent &:hover - color rgba(#000, 0.6) + color isDark ? #b2c1d5 : rgba(#000, 0.6) &:active - color darken(#000, 30%) + color isDark ? #b2c1d5 : darken(#000, 30%) > [data-fa] padding 0 @@ -632,4 +627,10 @@ export default Vue.extend({ > .main > .body > .content height calc(100% - 40px) +.mk-window[data-darkmode] + root(true) + +.mk-window:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/pages/favorites.vue b/src/client/app/desktop/views/pages/favorites.vue new file mode 100644 index 0000000000..d908c08f7c --- /dev/null +++ b/src/client/app/desktop/views/pages/favorites.vue @@ -0,0 +1,73 @@ +<template> +<mk-ui> + <main v-if="!fetching"> + <template v-for="favorite in favorites"> + <mk-note-detail :note="favorite.note" :key="favorite.note.id"/> + </template> + <a v-if="existMore" @click="more">さらに読み込む</a> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Progress from '../../../common/scripts/loading'; + +export default Vue.extend({ + data() { + return { + fetching: true, + favorites: [], + existMore: false, + moreFetching: false + }; + }, + created() { + this.fetch(); + }, + methods: { + fetch() { + Progress.start(); + this.fetching = true; + + (this as any).api('i/favorites', { + limit: 11 + }).then(favorites => { + if (favorites.length == 11) { + this.existMore = true; + favorites.pop(); + } + + this.favorites = favorites; + this.fetching = false; + + Progress.done(); + }); + }, + more() { + this.moreFetching = true; + (this as any).api('i/favorites', { + limit: 11, + maxId: 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 + margin 0 auto + padding 16px + max-width 700px +</style> diff --git a/src/client/app/desktop/views/pages/note.vue b/src/client/app/desktop/views/pages/note.vue index e92b0ff105..8502dd3d58 100644 --- a/src/client/app/desktop/views/pages/note.vue +++ b/src/client/app/desktop/views/pages/note.vue @@ -1,9 +1,11 @@ <template> <mk-ui> <main v-if="!fetching"> - <a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:@next%</a> <mk-note-detail :note="note"/> - <a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:@prev%</a> + <footer> + <router-link v-if="note.next" :to="note.next">%fa:angle-left% %i18n:@next%</router-link> + <router-link v-if="note.prev" :to="note.prev">%i18n:@prev% %fa:angle-right%</router-link> + </footer> </main> </mk-ui> </template> @@ -48,17 +50,12 @@ main padding 16px text-align center - > a - display inline-block + > footer + margin-top 16px - &:first-child - margin-bottom 4px - - &:last-child - margin-top 4px - - > [data-fa] - margin-right 4px + > a + display inline-block + margin 0 16px > .mk-note-detail margin 0 auto diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue index 698154e667..67e1e3bfe0 100644 --- a/src/client/app/desktop/views/pages/search.vue +++ b/src/client/app/desktop/views/pages/search.vue @@ -114,7 +114,7 @@ export default Vue.extend({ .notes max-width 600px margin 0 auto - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px overflow hidden diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue new file mode 100644 index 0000000000..4236cdbb14 --- /dev/null +++ b/src/client/app/desktop/views/pages/user-list.users.vue @@ -0,0 +1,124 @@ +<template> +<div> + <mk-widget-container> + <template slot="header">%fa:users% ユーザー</template> + <button slot="func" title="ユーザーを追加" @click="add">%fa:plus%</button> + + <div data-id="d0b63759-a822-4556-a5ce-373ab966e08a"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p> + <template v-else-if="users.length != 0"> + <div class="user" v-for="_user in users"> + <mk-avatar class="avatar" :user="_user"/> + <div class="body"> + <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link> + <p class="username">@{{ _user | acct }}</p> + </div> + </div> + </template> + <p class="empty" v-else>%i18n:@no-one%</p> + </div> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + list: { + type: Object, + required: true + } + }, + data() { + return { + fetching: true, + users: [] + }; + }, + mounted() { + (this as any).api('users/show', { + userIds: this.list.userIds + }).then(users => { + this.users = users; + this.fetching = false; + }); + }, + methods: { + add() { + (this as any).apis.input({ + title: 'ユーザー名', + }).then(async username => { + const user = await (this as any).api('users/show', { + username + }); + + (this as any).api('users/lists/push', { + listId: this.list.id, + userId: user.id + }); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + > .user + padding 16px + border-bottom solid 1px isDark ? #1c2023 : #eee + + &:last-child + border-bottom none + + &:after + content "" + display block + clear both + + > .avatar + display block + float left + margin 0 12px 0 0 + width 42px + height 42px + border-radius 8px + + > .body + float left + width calc(100% - 54px) + + > .name + margin 0 + font-size 16px + line-height 24px + color isDark ? #fff : #555 + + > .username + display block + margin 0 + font-size 15px + line-height 16px + color isDark ? #606984 : #ccc + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + +[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"][data-darkmode] + root(true) + +[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"]:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/user-list.vue b/src/client/app/desktop/views/pages/user-list.vue new file mode 100644 index 0000000000..2241b84e5e --- /dev/null +++ b/src/client/app/desktop/views/pages/user-list.vue @@ -0,0 +1,71 @@ +<template> +<mk-ui> + <div v-if="!fetching" data-id="02010e15-cc48-4245-8636-16078a9b623c"> + <div> + <div><h1>{{ list.title }}</h1></div> + <x-users :list="list"/> + </div> + <main> + <mk-user-list-timeline :list="list"/> + </main> + </div> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XUsers from './user-list.users.vue'; + +export default Vue.extend({ + components: { + XUsers + }, + data() { + return { + fetching: true, + list: null + }; + }, + watch: { + $route: 'fetch' + }, + mounted() { + this.fetch(); + }, + methods: { + fetch() { + this.fetching = true; + + (this as any).api('users/lists/show', { + listId: this.$route.params.list + }).then(list => { + this.list = list; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +[data-id="02010e15-cc48-4245-8636-16078a9b623c"] + display flex + justify-content center + margin 0 auto + max-width 1200px + + > main + > div > div + > *:not(:last-child) + margin-bottom 16px + + > main + padding 16px + width calc(100% - 275px * 2) + + > div + width 275px + margin 0 + padding 16px 0 16px 16px + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue index 9ccbc7a310..4c1b91e7a6 100644 --- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue +++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue @@ -38,7 +38,7 @@ export default Vue.extend({ <style lang="stylus" scoped> .followers-you-know background #fff - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px > .title @@ -49,7 +49,7 @@ export default Vue.extend({ font-size 0.9em font-weight bold color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) + box-shadow 0 1px rgba(#000, 0.07) > i margin-right 4px diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue index 203f936478..4af0f0bca6 100644 --- a/src/client/app/desktop/views/pages/user/user.friends.vue +++ b/src/client/app/desktop/views/pages/user/user.friends.vue @@ -4,9 +4,7 @@ <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p> <template v-if="!fetching && users.length != 0"> <div class="user" v-for="friend in users"> - <router-link class="avatar-anchor" :to="friend | userPage"> - <img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/> - </router-link> + <mk-avatar class="avatar" :user="friend"/> <div class="body"> <router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link> <p class="username">@{{ friend | acct }}</p> @@ -44,7 +42,7 @@ export default Vue.extend({ <style lang="stylus" scoped> .friends background #fff - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px > .title @@ -55,7 +53,7 @@ export default Vue.extend({ font-size 0.9em font-weight bold color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) + box-shadow 0 1px rgba(#000, 0.07) > i margin-right 4px @@ -82,18 +80,13 @@ export default Vue.extend({ display block clear both - > .avatar-anchor + > .avatar display block float left margin 0 12px 0 0 - - > .avatar - display block - width 42px - height 42px - margin 0 - border-radius 8px - vertical-align bottom + width 42px + height 42px + border-radius 8px > .body float left diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue index 7a0672d3d7..60dc15b15d 100644 --- a/src/client/app/desktop/views/pages/user/user.header.vue +++ b/src/client/app/desktop/views/pages/user/user.header.vue @@ -1,12 +1,13 @@ <template> <div class="header" :data-is-dark-background="user.bannerUrl != null"> - <div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote% <a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div> - <div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''"> - <div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div> + <div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div> + <div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div> + <div class="banner-container" :style="style"> + <div class="banner" ref="banner" :style="style" @click="onBannerClick"></div> + <div class="fade"></div> </div> - <div class="fade"></div> <div class="container"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/> + <mk-avatar class="avatar" :user="user" :disable-preview="true"/> <div class="title"> <p class="name">{{ user | userName }}</p> <p class="username">@{{ user | acct }}</p> @@ -24,6 +25,15 @@ import Vue from 'vue'; export default Vue.extend({ props: ['user'], + computed: { + style(): any { + if (this.user.bannerUrl == null) return {}; + return { + backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundImage: `url(${ this.user.bannerUrl })` + }; + } + }, mounted() { if (this.user.bannerUrl) { window.addEventListener('load', this.onScroll); @@ -67,21 +77,27 @@ export default Vue.extend({ @import '~const.styl' .header - $banner-height = 320px $footer-height = 58px overflow hidden background #f7f7f7 - box-shadow 0 1px 1px rgba(0, 0, 0, 0.075) + box-shadow 0 1px 1px rgba(#000, 0.075) + > .is-suspended > .is-remote - padding 16px - color #573c08 - background #fff0db + &.is-suspended + color #570808 + background #ffdbdb + + &.is-remote + color #573c08 + background #fff0db > p margin 0 auto - max-width 1024px + padding 14px 16px + max-width 1200px + font-size 14px > a font-weight bold @@ -91,8 +107,8 @@ export default Vue.extend({ > .banner background-color #383838 - > .fade - background linear-gradient(transparent, rgba(0, 0, 0, 0.7)) + > .fade + background linear-gradient(transparent, rgba(#000, 0.7)) > .container > .title @@ -102,7 +118,7 @@ export default Vue.extend({ text-shadow 0 0 8px #000 > .banner-container - height $banner-height + height 320px overflow hidden background-size cover background-position center @@ -113,14 +129,12 @@ export default Vue.extend({ background-size cover background-position center - > .fade - $fade-hight = 78px - - position absolute - top ($banner-height - $fade-hight) - left 0 - width 100% - height $fade-hight + > .fade + position absolute + bottom 0 + left 0 + width 100% + height 78px > .container max-width 1200px @@ -134,10 +148,9 @@ export default Vue.extend({ z-index 2 width 160px height 160px - margin 0 border solid 3px #fff border-radius 8px - box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2) + box-shadow 1px 1px 3px rgba(#000, 0.2) > .title position absolute diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue index 7ca520ea7f..6b242a6129 100644 --- a/src/client/app/desktop/views/pages/user/user.home.vue +++ b/src/client/app/desktop/views/pages/user/user.home.vue @@ -65,7 +65,7 @@ export default Vue.extend({ width calc(100% - 275px * 2) > .timeline - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px > div @@ -91,7 +91,7 @@ export default Vue.extend({ font-size 12px color #aaa background #fff - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px a diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index 9f749d5cc9..01c4c7b31e 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -41,7 +41,7 @@ export default Vue.extend({ <style lang="stylus" scoped> .photos background #fff - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px > .title @@ -52,7 +52,7 @@ export default Vue.extend({ font-size 0.9em font-weight bold color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) + box-shadow 0 1px rgba(#000, 0.07) > i margin-right 4px diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue index 72750e1b3d..29e49f36a6 100644 --- a/src/client/app/desktop/views/pages/user/user.profile.vue +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -3,8 +3,17 @@ <div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id"> <mk-follow-button :user="user" size="big"/> <p class="followed" v-if="user.isFollowed">%i18n:@follows-you%</p> - <p v-if="user.isMuted">%i18n:@muted% <a @click="unmute">%i18n:@unmute%</a></p> - <p v-if="!user.isMuted"><a @click="mute">%i18n:@mute%</a></p> + <p class="stalk" v-if="user.isFollowing"> + <span v-if="user.isStalking">%i18n:@stalking% <a @click="unstalk">%fa:meh% %i18n:@unstalk%</a></span> + <span v-if="!user.isStalking"><a @click="stalk">%fa:user-secret% %i18n:@stalk%</a></span> + </p> + </div> + <div class="action-form"> + <button class="mute ui" @click="user.isMuted ? unmute() : mute()"> + <span v-if="user.isMuted">%fa:eye% %i18n:@unmute%</span> + <span v-if="!user.isMuted">%fa:eye-slash% %i18n:@mute%</span> + </button> + <button class="mute ui" @click="list">%fa:list% リストに追加</button> </div> <div class="description" v-if="user.description">{{ user.description }}</div> <div class="birthday" v-if="user.host === null && user.profile.birthday"> @@ -26,6 +35,7 @@ import Vue from 'vue'; import * as age from 's-age'; import MkFollowingWindow from '../../components/following-window.vue'; import MkFollowersWindow from '../../components/followers-window.vue'; +import MkUserListsWindow from '../../components/user-lists-window.vue'; export default Vue.extend({ props: ['user'], @@ -47,6 +57,26 @@ export default Vue.extend({ }); }, + stalk() { + (this as any).api('following/stalk', { + userId: this.user.id + }).then(() => { + this.user.isStalking = true; + }, () => { + alert('error'); + }); + }, + + unstalk() { + (this as any).api('following/unstalk', { + userId: this.user.id + }).then(() => { + this.user.isStalking = false; + }, () => { + alert('error'); + }); + }, + mute() { (this as any).api('mute/create', { userId: this.user.id @@ -65,6 +95,21 @@ export default Vue.extend({ }, () => { alert('error'); }); + }, + + list() { + const w = (this as any).os.new(MkUserListsWindow); + w.$once('choosen', async list => { + w.close(); + await (this as any).api('users/lists/push', { + listId: list.id, + userId: this.user.id + }); + (this as any).apis.dialog({ + title: 'Done!', + text: `${this.user.name}を${list.title}に追加しました。` + }); + }); } } }); @@ -73,7 +118,7 @@ export default Vue.extend({ <style lang="stylus" scoped> .profile background #fff - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px > *:first-child @@ -81,11 +126,9 @@ export default Vue.extend({ > .friend-form padding 16px + text-align center border-top solid 1px #eee - > .mk-big-follow-button - width 100% - > .followed margin 12px 0 0 0 padding 0 @@ -96,6 +139,20 @@ export default Vue.extend({ background #eefaff border-radius 4px + > .stalk + margin 12px 0 0 0 + + > .action-form + padding 16px + text-align center + border-top solid 1px #eee + + > * + width 100% + + &:not(:last-child) + margin-bottom 12px + > .description padding 16px color #555 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 55d6072a9d..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,42 +1,36 @@ <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/> </div> - <p class="empty" v-if="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p> - <mk-notes ref="timeline" :notes="notes"> - <div slot="footer"> - <template v-if="!moreFetching">%fa:moon%</template> - <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> - </div> + <mk-notes ref="timeline" :more="existMore ? more : null"> + <p class="empty" slot="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p> </mk-notes> </div> </template> <script lang="ts"> import Vue from 'vue'; + +const fetchLimit = 10; + export default Vue.extend({ props: ['user'], data() { return { fetching: true, moreFetching: false, + existMore: false, mode: 'default', unreadCount: 0, - notes: [], date: null }; }, - computed: { - empty(): boolean { - return this.notes.length == 0; - } - }, watch: { mode() { this.fetch(); @@ -44,13 +38,11 @@ export default Vue.extend({ }, mounted() { document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); this.fetch(() => this.$emit('loaded')); }, beforeDestroy() { document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); }, methods: { onDocumentKeydown(e) { @@ -61,36 +53,43 @@ export default Vue.extend({ } }, fetch(cb?) { - (this as any).api('users/notes', { - userId: this.user.id, - untilDate: this.date ? this.date.getTime() : undefined, - includeReplies: this.mode == 'with-replies', - withMedia: this.mode == 'with-media' - }).then(notes => { - this.notes = notes; - this.fetching = false; - if (cb) cb(); - }); + this.fetching = true; + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('users/notes', { + userId: this.user.id, + limit: fetchLimit + 1, + untilDate: this.date ? this.date.getTime() : undefined, + includeReplies: this.mode == 'with-replies', + withMedia: this.mode == 'with-media' + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + if (cb) cb(); + }, rej); + })); }, more() { - if (this.moreFetching || this.fetching || this.notes.length == 0) return; this.moreFetching = true; (this as any).api('users/notes', { userId: this.user.id, + limit: fetchLimit + 1, includeReplies: this.mode == 'with-replies', withMedia: this.mode == 'with-media', - untilId: this.notes[this.notes.length - 1].id + untilId: (this.$refs.timeline as any).tail().id }).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; - this.notes = this.notes.concat(notes); }); }, - onScroll() { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 16/*遊び*/) { - this.more(); - } - }, warp(date) { this.date = date; this.fetch(); @@ -115,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/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index 93d17b58fe..898b6b2179 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -8,9 +8,7 @@ <p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p> <p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p> <div class="users"> - <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="user | userPage" v-user-preview="user.id"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/> </div> </div> <div> @@ -125,7 +123,8 @@ export default Vue.extend({ flex 1 $width = 1000px - background-image url('/assets/welcome-bg.svg') + background linear-gradient(to bottom, #1e1d65, #bd6659) + //background-image url('/assets/welcome-bg.svg') background-size cover background-position top center @@ -216,13 +215,9 @@ export default Vue.extend({ > * display inline-block margin 4px - - > * - display inline-block - width 38px - height 38px - vertical-align top - border-radius 6px + width 38px + height 38px + border-radius 6px > div:last-child @@ -230,14 +225,14 @@ export default Vue.extend({ width 410px background #fff border-radius 8px - box-shadow 0 0 0 12px rgba(0, 0, 0, 0.1) + box-shadow 0 0 0 12px rgba(#000, 0.1) overflow hidden > header z-index 1 padding 12px 16px color #888d94 - box-shadow 0 1px 0px rgba(0, 0, 0, 0.1) + box-shadow 0 1px 0px rgba(#000, 0.1) > div position absolute @@ -309,9 +304,3 @@ export default Vue.extend({ a color #666 </style> - -<style lang="stylus"> -html -body - background linear-gradient(to bottom, #1e1d65, #bd6659) -</style> diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue index 0bdf4622af..1be87f590c 100644 --- a/src/client/app/desktop/views/widgets/activity.vue +++ b/src/client/app/desktop/views/widgets/activity.vue @@ -22,9 +22,11 @@ export default define({ } else { this.props.design++; } + this.save(); }, viewChanged(view) { this.props.view = view; + this.save(); } } }); diff --git a/src/client/app/desktop/views/widgets/channel.vue b/src/client/app/desktop/views/widgets/channel.vue index 7e96f8ee3d..d21aed40fd 100644 --- a/src/client/app/desktop/views/widgets/channel.vue +++ b/src/client/app/desktop/views/widgets/channel.vue @@ -37,6 +37,7 @@ export default define({ methods: { func() { this.props.compact = !this.props.compact; + this.save(); }, settings() { const id = window.prompt('チャンネルID'); @@ -61,7 +62,7 @@ export default define({ <style lang="stylus" scoped> .mkw-channel background #fff - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px overflow hidden @@ -73,7 +74,7 @@ export default define({ font-size 0.9em font-weight bold color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) + box-shadow 0 1px rgba(#000, 0.07) > [data-fa] margin-right 4px diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue index 0f197fb2d7..791d2ff1bb 100644 --- a/src/client/app/desktop/views/widgets/messaging.vue +++ b/src/client/app/desktop/views/widgets/messaging.vue @@ -1,13 +1,18 @@ <template> <div class="mkw-messaging"> - <p class="title" v-if="props.design == 0">%fa:comments%%i18n:@title%</p> - <mk-messaging ref="index" compact @navigate="navigate"/> + <mk-widget-container :show-header="props.design == 0"> + <template slot="header">%fa:comments%%i18n:@title%</template> + <button slot="func" @click="add">%fa:plus%</button> + + <mk-messaging ref="index" compact @navigate="navigate"/> + </mk-widget-container> </div> </template> <script lang="ts"> import define from '../../../common/define-widget'; import MkMessagingRoomWindow from '../components/messaging-room-window.vue'; +import MkMessagingWindow from '../components/messaging-window.vue'; export default define({ name: 'messaging', @@ -21,12 +26,16 @@ export default define({ user: user }); }, + add() { + (this as any).os.new(MkMessagingWindow); + }, func() { if (this.props.design == 1) { this.props.design = 0; } else { this.props.design++; } + this.save(); } } }); @@ -34,25 +43,7 @@ export default define({ <style lang="stylus" scoped> .mkw-messaging - overflow hidden - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 2 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > .mk-messaging + .mk-messaging max-height 250px overflow auto diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue index 0c2fa0434d..f75a091480 100644 --- a/src/client/app/desktop/views/widgets/notifications.vue +++ b/src/client/app/desktop/views/widgets/notifications.vue @@ -1,10 +1,11 @@ <template> <div class="mkw-notifications"> - <template v-if="!props.compact"> - <p class="title">%fa:R bell%%i18n:@title%</p> - <button @click="settings" title="%i18n:@settings%">%fa:cog%</button> - </template> - <mk-notifications/> + <mk-widget-container :show-header="!props.compact"> + <template slot="header">%fa:R bell%%i18n:@title%</template> + <button slot="func" title="%i18n:@settings%" @click="settings">%fa:cog%</button> + + <mk-notifications :class="$style.notifications"/> + </mk-widget-container> </div> </template> @@ -22,49 +23,15 @@ export default define({ }, func() { this.props.compact = !this.props.compact; + this.save(); } } }); </script> -<style lang="stylus" scoped> -.mkw-notifications - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - z-index 1 - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .mk-notifications - max-height 300px - overflow auto +<style lang="stylus" module> +.notifications + max-height 300px + overflow auto </style> diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue index 6cb1192c24..36fcc20636 100644 --- a/src/client/app/desktop/views/widgets/polls.vue +++ b/src/client/app/desktop/views/widgets/polls.vue @@ -1,16 +1,19 @@ <template> <div class="mkw-polls"> - <template v-if="!props.compact"> - <p class="title">%fa:chart-pie%%i18n:@title%</p> - <button @click="fetch" title="%i18n:@refresh%">%fa:sync%</button> - </template> - <div class="poll" v-if="!fetching && poll != null"> - <p v-if="poll.text"><router-link to="poll | notePage">{{ poll.text }}</router-link></p> - <p v-if="!poll.text"><router-link to="poll | notePage">%fa:link%</router-link></p> - <mk-poll :note="poll"/> - </div> - <p class="empty" v-if="!fetching && poll == null">%i18n:@nothing%</p> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <mk-widget-container :show-header="!props.compact"> + <template slot="header">%fa:chart-pie%%i18n:@title%</template> + <button slot="func" title="%i18n:@refresh%" @click="fetch">%fa:sync%</button> + + <div class="mkw-polls--body" :data-darkmode="_darkmode_"> + <div class="poll" v-if="!fetching && poll != null"> + <p v-if="poll.text"><router-link to="poll | notePage">{{ poll.text }}</router-link></p> + <p v-if="!poll.text"><router-link to="poll | notePage">%fa:link%</router-link></p> + <mk-poll :note="poll"/> + </div> + <p class="empty" v-if="!fetching && poll == null">%i18n:@nothing%</p> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + </div> + </mk-widget-container> </div> </template> @@ -36,6 +39,7 @@ export default define({ methods: { func() { this.props.compact = !this.props.compact; + this.save(); }, fetch() { this.fetching = true; @@ -60,44 +64,11 @@ export default define({ </script> <style lang="stylus" scoped> -.mkw-polls - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - +root(isDark) > .poll padding 16px font-size 12px - color #555 + color isDark ? #9ea4ad : #555 > p margin 0 0 8px 0 @@ -120,4 +91,10 @@ export default define({ > [data-fa] margin-right 4px +.mkw-polls--body[data-darkmode] + root(true) + +.mkw-polls--body:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue index 627943588f..69b21ad37a 100644 --- a/src/client/app/desktop/views/widgets/post-form.vue +++ b/src/client/app/desktop/views/widgets/post-form.vue @@ -29,6 +29,7 @@ export default define({ } else { this.props.design++; } + this.save(); }, onKeydown(e) { if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); @@ -59,7 +60,7 @@ export default define({ .mkw-post-form background #fff overflow hidden - border solid 1px rgba(0, 0, 0, 0.075) + border solid 1px rgba(#000, 0.075) border-radius 6px > .title @@ -70,7 +71,7 @@ export default define({ font-size 0.9em font-weight bold color #888 - box-shadow 0 1px rgba(0, 0, 0, 0.07) + box-shadow 0 1px rgba(#000, 0.07) > [data-fa] margin-right 4px diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue index 1b4b11de3c..3b01ed034d 100644 --- a/src/client/app/desktop/views/widgets/profile.vue +++ b/src/client/app/desktop/views/widgets/profile.vue @@ -8,12 +8,9 @@ title="クリックでバナー編集" @click="os.apis.updateBanner" ></div> - <img class="avatar" - :src="`${os.i.avatarUrl}?thumbnail&size=96`" + <mk-avatar class="avatar" :user="os.i" @click="os.apis.updateAvatar" - alt="avatar" title="クリックでアバター編集" - v-user-preview="os.i.id" /> <router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link> <p class="username">@{{ os.i | acct }}</p> @@ -36,16 +33,17 @@ export default define({ } else { this.props.design++; } + this.save(); } } }); </script> <style lang="stylus" scoped> -.mkw-profile +root(isDark) overflow hidden - background #fff - border solid 1px rgba(0, 0, 0, 0.075) + background isDark ? #282c37 : #fff + border solid 1px rgba(#000, 0.075) border-radius 6px &[data-compact] @@ -54,14 +52,14 @@ export default define({ display block width 100% height 100% - background rgba(0, 0, 0, 0.5) + background rgba(#000, 0.5) > .avatar top ((100px - 58px) / 2) left ((100px - 58px) / 2) border none border-radius 100% - box-shadow 0 0 16px rgba(0, 0, 0, 0.5) + box-shadow 0 0 16px rgba(#000, 0.5) > .name position absolute @@ -70,7 +68,7 @@ export default define({ margin 0 line-height 100px color #fff - text-shadow 0 0 8px rgba(0, 0, 0, 0.5) + text-shadow 0 0 8px rgba(#000, 0.5) > .username display none @@ -91,7 +89,7 @@ export default define({ > .banner height 100px - background-color #f5f5f5 + background-color isDark ? #303e4a : #f5f5f5 background-size cover background-position center cursor pointer @@ -103,10 +101,8 @@ export default define({ left 16px width 58px height 58px - margin 0 - border solid 3px #fff + border solid 3px isDark ? #282c37 : #fff border-radius 8px - vertical-align bottom cursor pointer > .name @@ -114,13 +110,19 @@ export default define({ margin 10px 0 0 84px line-height 16px font-weight bold - color #555 + color isDark ? #fff : #555 > .username display block margin 4px 0 8px 84px line-height 16px font-size 0.9em - color #999 + color isDark ? #606984 : #999 + +.mkw-profile[data-darkmode] + root(true) + +.mkw-profile:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue index 6db3b14c62..22a4120403 100644 --- a/src/client/app/desktop/views/widgets/timemachine.vue +++ b/src/client/app/desktop/views/widgets/timemachine.vue @@ -22,6 +22,7 @@ export default define({ } else { this.props.design++; } + this.save(); } } }); diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue index fccda3f9d0..c33bf2f2f2 100644 --- a/src/client/app/desktop/views/widgets/trends.vue +++ b/src/client/app/desktop/views/widgets/trends.vue @@ -1,15 +1,18 @@ <template> <div class="mkw-trends"> - <template v-if="!props.compact"> - <p class="title">%fa:fire%%i18n:@title%</p> - <button @click="fetch" title="%i18n:@refresh%">%fa:sync%</button> - </template> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <div class="note" v-else-if="note != null"> - <p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p> - <p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p> - </div> - <p class="empty" v-else>%i18n:@nothing%</p> + <mk-widget-container :show-header="!props.compact"> + <template slot="header">%fa:fire%%i18n:@title%</template> + <button slot="func" title="%i18n:@refresh%" @click="fetch">%fa:sync%</button> + + <div class="mkw-trends--body"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <div class="note" v-else-if="note != null"> + <p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p> + <p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p> + </div> + <p class="empty" v-else>%i18n:@nothing%</p> + </div> + </mk-widget-container> </div> </template> @@ -35,6 +38,7 @@ export default define({ methods: { func() { this.props.compact = !this.props.compact; + this.save(); }, fetch() { this.fetching = true; @@ -63,67 +67,41 @@ export default define({ </script> <style lang="stylus" scoped> -.mkw-trends - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee +root(isDark) + .mkw-trends--body + > .note + padding 16px + font-size 12px + font-style oblique + color #555 - > [data-fa] - margin-right 4px + > p + margin 0 - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc + > .text, + > .author + > a + color inherit - &:hover + > .empty + margin 0 + padding 16px + text-align center color #aaa - &:active - color #999 - - > .note - padding 16px - font-size 12px - font-style oblique - color #555 - - > p + > .fetching margin 0 + padding 16px + text-align center + color #aaa - > .text, - > .author - > a - color inherit - - > .empty - margin 0 - padding 16px - text-align center - color #aaa + > [data-fa] + margin-right 4px - > .fetching - margin 0 - padding 16px - text-align center - color #aaa +.mkw-trends[data-darkmode] + root(true) - > [data-fa] - margin-right 4px +.mkw-trends:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue index 0955ebbd71..328fa56697 100644 --- a/src/client/app/desktop/views/widgets/users.vue +++ b/src/client/app/desktop/views/widgets/users.vue @@ -1,23 +1,24 @@ <template> <div class="mkw-users"> - <template v-if="!props.compact"> - <p class="title">%fa:users%%i18n:@title%</p> - <button @click="refresh" title="%i18n:@refresh%">%fa:sync%</button> - </template> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <template v-else-if="users.length != 0"> - <div class="user" v-for="_user in users"> - <router-link class="avatar-anchor" :to="_user | userPage"> - <img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/> - </router-link> - <div class="body"> - <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link> - <p class="username">@{{ _user | acct }}</p> - </div> - <mk-follow-button :user="_user"/> + <mk-widget-container :show-header="!props.compact"> + <template slot="header">%fa:users%%i18n:@title%</template> + <button slot="func" title="%i18n:@refresh%" @click="refresh">%fa:sync%</button> + + <div class="mkw-users--body"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <template v-else-if="users.length != 0"> + <div class="user" v-for="_user in users"> + <mk-avatar class="avatar" :user="_user"/> + <div class="body"> + <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link> + <p class="username">@{{ _user | acct }}</p> + </div> + <mk-follow-button :user="_user"/> + </div> + </template> + <p class="empty" v-else>%i18n:@no-one%</p> </div> - </template> - <p class="empty" v-else>%i18n:@no-one%</p> + </mk-widget-container> </div> </template> @@ -45,6 +46,7 @@ export default define({ methods: { func() { this.props.compact = !this.props.compact; + this.save(); }, fetch() { this.fetching = true; @@ -71,100 +73,69 @@ export default define({ </script> <style lang="stylus" scoped> -.mkw-users - background #fff - border solid 1px rgba(0, 0, 0, 0.075) - border-radius 6px - - > .title - margin 0 - padding 0 16px - line-height 42px - font-size 0.9em - font-weight bold - color #888 - border-bottom solid 1px #eee - - > [data-fa] - margin-right 4px - - > button - position absolute - z-index 2 - top 0 - right 0 - padding 0 - width 42px - font-size 0.9em - line-height 42px - color #ccc - - &:hover - color #aaa - - &:active - color #999 - - > .user - padding 16px - border-bottom solid 1px #eee +root(isDark) + .mkw-users--body + > .user + padding 16px + border-bottom solid 1px isDark ? #1c2023 : #eee - &:last-child - border-bottom none + &:last-child + border-bottom none - &:after - content "" - display block - clear both - - > .avatar-anchor - display block - float left - margin 0 12px 0 0 + &:after + content "" + display block + clear both > .avatar display block + float left + margin 0 12px 0 0 width 42px height 42px - margin 0 border-radius 8px - vertical-align bottom - > .body - float left - width calc(100% - 54px) + > .body + float left + width calc(100% - 54px) - > .name - margin 0 - font-size 16px - line-height 24px - color #555 + > .name + margin 0 + font-size 16px + line-height 24px + color isDark ? #fff : #555 - > .username - display block - margin 0 - font-size 15px - line-height 16px - color #ccc + > .username + display block + margin 0 + font-size 15px + line-height 16px + color isDark ? #606984 : #ccc + + > .mk-follow-button + position absolute + top 16px + right 16px - > .mk-follow-button - position absolute - top 16px - right 16px + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa - > .empty - margin 0 - padding 16px - text-align center - color #aaa + > [data-fa] + margin-right 4px - > .fetching - margin 0 - padding 16px - text-align center - color #aaa +.mkw-users[data-darkmode] + root(true) - > [data-fa] - margin-right 4px +.mkw-users:not([data-darkmode]) + root(false) </style> |