diff options
Diffstat (limited to 'src/client/app/mobile')
45 files changed, 1768 insertions, 1100 deletions
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index 1de4891973..2e9805e0d0 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -55,15 +55,15 @@ init((launch) => { { path: '/signup', name: 'signup', component: MkSignup }, { path: '/i/settings', component: MkSettings }, { path: '/i/settings/profile', component: MkProfileSetting }, - { path: '/i/notifications', component: MkNotifications }, - { path: '/i/messaging', component: MkMessaging }, + { path: '/i/notifications', name: 'notifications', component: MkNotifications }, + { path: '/i/messaging', name: 'messaging', component: MkMessaging }, { path: '/i/messaging/:user', component: MkMessagingRoom }, - { path: '/i/drive', component: MkDrive }, + { path: '/i/drive', name: 'drive', component: MkDrive }, { path: '/i/drive/folder/:folder', component: MkDrive }, { path: '/i/drive/file/:file', component: MkDrive }, { path: '/selectdrive', component: MkSelectDrive }, { path: '/search', component: MkSearch }, - { path: '/othello', component: MkOthello }, + { path: '/othello', name: 'othello', component: MkOthello }, { path: '/othello/:game', component: MkOthello }, { path: '/@:user', component: MkUser }, { path: '/@:user/followers', component: MkFollowers }, diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl index 81912a2483..847ae8eec5 100644 --- a/src/client/app/mobile/style.styl +++ b/src/client/app/mobile/style.styl @@ -8,6 +8,10 @@ html height 100% + background #ececed + + &[data-darkmode] + background #191B22 body display flex diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue index 41536afbd4..d95d5fa223 100644 --- a/src/client/app/mobile/views/components/drive-file-chooser.vue +++ b/src/client/app/mobile/views/components/drive-file-chooser.vue @@ -54,7 +54,7 @@ export default Vue.extend({ width 100% height 100% padding 8px - background rgba(0, 0, 0, 0.2) + background rgba(#000, 0.2) > .body width 100% diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue index bfd8fbda6f..7934fb7816 100644 --- a/src/client/app/mobile/views/components/drive-folder-chooser.vue +++ b/src/client/app/mobile/views/components/drive-folder-chooser.vue @@ -38,7 +38,7 @@ export default Vue.extend({ width 100% height 100% padding 8px - background rgba(0, 0, 0, 0.2) + background rgba(#000, 0.2) > .body width 100% diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue index c7be7d1879..764822e98c 100644 --- a/src/client/app/mobile/views/components/drive.file-detail.vue +++ b/src/client/app/mobile/views/components/drive.file-detail.vue @@ -139,7 +139,7 @@ export default Vue.extend({ max-width 100% max-height 300px margin 0 auto - box-shadow 1px 1px 4px rgba(0, 0, 0, 0.2) + box-shadow 1px 1px 4px rgba(#000, 0.2) > footer padding 8px 8px 0 8px @@ -226,7 +226,7 @@ export default Vue.extend({ background-color #767676 background-image none border-color #444 - box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2) + box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2) > [data-fa] margin-right 4px diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue index 7aa666e1bb..ef3432a3ec 100644 --- a/src/client/app/mobile/views/components/drive.vue +++ b/src/client/app/mobile/views/components/drive.vue @@ -474,11 +474,11 @@ export default Vue.extend({ overflow auto white-space nowrap font-size 0.9em - color rgba(0, 0, 0, 0.67) + color rgba(#000, 0.67) -webkit-backdrop-filter blur(12px) backdrop-filter blur(12px) background-color rgba(#fff, 0.75) - border-bottom solid 1px rgba(0, 0, 0, 0.13) + border-bottom solid 1px rgba(#000, 0.13) > p > a @@ -555,7 +555,7 @@ export default Vue.extend({ display inline-block position absolute top 0 - background rgba(0, 0, 0, 0.2) + background rgba(#000, 0.2) border-radius 100% animation sk-bounce 2.0s infinite ease-in-out diff --git a/src/client/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue index 961a5f568a..ba4abe341f 100644 --- a/src/client/app/mobile/views/components/friends-maker.vue +++ b/src/client/app/mobile/views/components/friends-maker.vue @@ -57,7 +57,7 @@ export default Vue.extend({ .mk-friends-maker background #fff border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 0 1px rgba(#000, 0.2) > .title margin 0 diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts index 9346700304..5ed8427b05 100644 --- a/src/client/app/mobile/views/components/index.ts +++ b/src/client/app/mobile/views/components/index.ts @@ -1,7 +1,6 @@ import Vue from 'vue'; import ui from './ui.vue'; -import timeline from './timeline.vue'; import note from './note.vue'; import notes from './notes.vue'; import mediaImage from './media-image.vue'; @@ -20,11 +19,11 @@ import notificationPreview from './notification-preview.vue'; import usersList from './users-list.vue'; import userPreview from './user-preview.vue'; import userTimeline from './user-timeline.vue'; +import userListTimeline from './user-list-timeline.vue'; import activity from './activity.vue'; import widgetContainer from './widget-container.vue'; Vue.component('mk-ui', ui); -Vue.component('mk-timeline', timeline); Vue.component('mk-note', note); Vue.component('mk-notes', notes); Vue.component('mk-media-image', mediaImage); @@ -43,5 +42,6 @@ Vue.component('mk-notification-preview', notificationPreview); Vue.component('mk-users-list', usersList); Vue.component('mk-user-preview', userPreview); Vue.component('mk-user-timeline', userTimeline); +Vue.component('mk-user-list-timeline', userListTimeline); Vue.component('mk-activity', activity); Vue.component('mk-widget-container', widgetContainer); diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue index cfc2134988..92d1cdc6f5 100644 --- a/src/client/app/mobile/views/components/media-image.vue +++ b/src/client/app/mobile/views/components/media-image.vue @@ -6,12 +6,20 @@ import Vue from '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)` }; } } diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue index 393fa9b831..89700b5e82 100644 --- a/src/client/app/mobile/views/components/note-card.vue +++ b/src/client/app/mobile/views/components/note-card.vue @@ -27,17 +27,17 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-note-card +root(isDark) display inline-block width 150px //height 120px font-size 12px - background #fff + background isDark ? #282c37 : #fff border-radius 4px > a display block - color #2c3940 + color isDark ? #fff : #2c3940 &:hover text-decoration none @@ -75,11 +75,17 @@ export default Vue.extend({ left 0 width 100% height 20px - background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%) + background isDark ? linear-gradient(to bottom, rgba(#282c37, 0) 0%, #282c37 100%) : linear-gradient(to bottom, rgba(#fff, 0) 0%, #fff 100%) > .mk-time display inline-block padding 8px color #aaa +.mk-note-card[data-darkmode] + root(true) + +.mk-note-card:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue index 06f442d308..e515fda8a6 100644 --- a/src/client/app/mobile/views/components/note-detail.sub.vue +++ b/src/client/app/mobile/views/components/note-detail.sub.vue @@ -1,8 +1,6 @@ <template> <div class="root sub"> - <router-link class="avatar-anchor" :to="note.user | userPage"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" :user="note.user"/> <div class="main"> <header> <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> @@ -27,35 +25,29 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.root.sub +root(isDark) padding 8px font-size 0.9em - background #fdfdfd + background isDark ? #21242d : #fdfdfd @media (min-width 500px) padding 12px + @media (min-width 600px) + padding 24px 32px + &:after content "" display block clear both - &:hover - > .main > footer > button - color #888 - - > .avatar-anchor + > .avatar display block float left margin 0 12px 0 0 - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 8px - vertical-align bottom + width 48px + height 48px + border-radius 8px > .main float left @@ -63,6 +55,7 @@ export default Vue.extend({ > header display flex + align-items baseline margin-bottom 4px white-space nowrap @@ -71,7 +64,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 700 text-align left @@ -84,11 +77,11 @@ export default Vue.extend({ > .username text-align left margin 0 .5em 0 0 - color #d1d8da + color isDark ? #606984 : #d1d8da > .time margin-left auto - color #b2b8bb + color isDark ? #606984 : #b2b8bb > .body @@ -97,7 +90,12 @@ export default Vue.extend({ margin 0 padding 0 font-size 1.1em - color #717171 + color isDark ? #959ba7 : #717171 -</style> +.root.sub[data-darkmode] + root(true) + +.root.sub:not([data-darkmode]) + root(false) +</style> diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index 7d2747751e..5a7226faac 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -17,29 +17,27 @@ </div> <div class="renote" v-if="isRenote"> <p> - <router-link class="avatar-anchor" :to="note.user | userPage"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> - </router-link> - %fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote + <mk-avatar class="avatar" :user="note.user"/>%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote </p> </div> <article> <header> - <router-link class="avatar-anchor" :to="p.user | userPage"> - <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" :user="p.user"/> <div> <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> <span class="username">@{{ p.user | acct }}</span> </div> </header> <div class="body"> - <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/> + <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="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> <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"/> @@ -55,7 +53,9 @@ <footer> <mk-reactions-viewer :note="p"/> <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="Renote"> %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> @@ -147,7 +147,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]); @@ -207,15 +207,18 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-note-detail +root(isDark) overflow hidden margin 0 auto padding 0 width 100% text-align left - background #fff + background isDark ? #282C37 : #fff border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 2px rgba(#000, 0.1) + + @media (min-width 500px) + box-shadow 0 8px 32px rgba(#000, 0.1) > .fetching padding 64px 0 @@ -229,45 +232,37 @@ 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 box-shadow none &:hover - background #f6f6f6 - - &:active - background #f0f0f0 + background isDark ? #16181d : #f6f6f6 &:disabled color #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 +274,7 @@ export default Vue.extend({ padding-top 8px > .reply-to - border-bottom 1px solid #eef0f2 + border-bottom 1px solid isDark ? #1c2023 : #eef0f2 > article padding 14px 16px 9px 16px @@ -292,36 +287,27 @@ export default Vue.extend({ display block clear both - &:hover - > .main > footer > button - color #888 - > header display flex - line-height 1.1 + line-height 1.1em - > .avatar-anchor + > .avatar display block - padding 0 .5em 0 0 - - > .avatar - display block - width 54px - height 54px - margin 0 - border-radius 8px - vertical-align bottom + margin 0 12px 0 0 + width 54px + height 54px + border-radius 8px - @media (min-width 500px) - width 60px - height 60px + @media (min-width 500px) + width 60px + height 60px > div > .name display inline-block margin .4em 0 - color #777 + color isDark ? #fff : #627079 font-size 16px font-weight bold text-align left @@ -334,11 +320,22 @@ export default Vue.extend({ display block text-align left margin 0 - color #ccc + color isDark ? #606984 : #ccc > .body padding 8px 0 + > .text + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 16px + color isDark ? #fff : #717171 + + @media (min-width 500px) + font-size 24px + > .renote margin 8px 0 @@ -394,7 +391,7 @@ export default Vue.extend({ > .time font-size 16px - color #c0c0c0 + color isDark ? #606984 : #c0c0c0 > footer font-size 1.2em @@ -406,14 +403,14 @@ export default Vue.extend({ border none box-shadow none font-size 1em - color #ddd + color isDark ? #606984 : #ddd cursor pointer &:not(:last-child) margin-right 28px &:hover - color #666 + color isDark ? #9198af : #666 > .count display inline @@ -425,20 +422,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) -<style lang="stylus" module> -.text - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 16px - color #717171 - - @media (min-width 500px) - font-size 24px +.mk-note-detail:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue index b9a6db315d..ec11f23315 100644 --- a/src/client/app/mobile/views/components/note-preview.vue +++ b/src/client/app/mobile/views/components/note-preview.vue @@ -1,8 +1,6 @@ <template> <div class="mk-note-preview"> - <router-link class="avatar-anchor" :to="note.user | userPage"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" :user="note.user"/> <div class="main"> <header> <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> @@ -27,33 +25,23 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-note-preview +root(isDark) margin 0 padding 0 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 12px 0 0 - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 8px - vertical-align bottom + width 48px + height 48px + border-radius 8px > .main float left @@ -61,6 +49,7 @@ export default Vue.extend({ > header display flex + align-items baseline margin-bottom 4px white-space nowrap @@ -69,7 +58,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 700 text-align left @@ -82,11 +71,11 @@ export default Vue.extend({ > .username text-align left margin 0 .5em 0 0 - color #d1d8da + color isDark ? #606984 : #d1d8da > .time margin-left auto - color #b2b8bb + color isDark ? #606984 : #b2b8bb > .body @@ -95,6 +84,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/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue index d489f3a053..82025291da 100644 --- a/src/client/app/mobile/views/components/note.sub.vue +++ b/src/client/app/mobile/views/components/note.sub.vue @@ -1,15 +1,22 @@ <template> <div class="sub"> - <router-link class="avatar-anchor" :to="note.user | userPage"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" :user="note.user"/> <div class="main"> <header> <router-link class="name" :to="note.user | userPage">{{ 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"/> @@ -27,34 +34,31 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.sub - font-size 0.9em +root(isDark) padding 16px + font-size 0.9em + background isDark ? #21242d : #fcfcfc + + @media (min-width 600px) + padding 24px 32px &:after content "" display block clear both - > .avatar-anchor + > .avatar display block float left margin 0 10px 0 0 + width 44px + height 44px + border-radius 8px @media (min-width 500px) margin-right 16px - - > .avatar - display block - width 44px - height 44px - margin 0 - border-radius 8px - vertical-align bottom - - @media (min-width 500px) - width 52px - height 52px + width 52px + height 52px > .main float left @@ -65,6 +69,7 @@ export default Vue.extend({ > header display flex + align-items baseline margin-bottom 2px white-space nowrap @@ -73,7 +78,7 @@ export default Vue.extend({ margin 0 0.5em 0 0 padding 0 overflow hidden - color #607073 + color isDark ? #fff : #607073 font-size 1em font-weight 700 text-align left @@ -86,24 +91,40 @@ export default Vue.extend({ > .username text-align left margin 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% -</style> +.sub[data-darkmode] + root(true) +.sub:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index cccb8875b4..d66f5a1016 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -1,24 +1,18 @@ <template> <div class="note" :class="{ renote: isRenote }"> - <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"> - <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - %fa:retweet% - <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span> - <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> - <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> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <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=96`" alt="avatar"/> - </router-link> + <mk-avatar class="avatar" :user="p.user"/> <div class="main"> <header> <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> @@ -29,36 +23,49 @@ <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 != null"><a 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.text" :text="p.text" :i="os.i" :class="$style.text"/> - <a class="rp" v-if="p.renote != null">RP:</a> + <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 != null">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> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> </div> - <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> - <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - <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> <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> - <div class="renote" v-if="p.renote"> - <mk-note-preview :note="p.renote"/> - </div> </div> <footer> <mk-reactions-viewer :note="p" ref="reactionsViewer"/> <button @click="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="Renote"> %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> @@ -92,6 +99,7 @@ export default Vue.extend({ data() { return { + showContent: false, connection: null, connectionId: null }; @@ -142,7 +150,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]); @@ -229,15 +237,9 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.note +root(isDark) font-size 12px - border-bottom solid 1px #eaeaea - - &:first-child - border-radius 8px 8px 0 0 - - > .renote - border-radius 8px 8px 0 0 + border-bottom solid 1px isDark ? #1c2023 : #eaeaea &:last-of-type border-bottom none @@ -249,83 +251,78 @@ export default Vue.extend({ font-size 16px > .renote + display flex + align-items center + padding 8px 16px + 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 8px 16px - line-height 28px + @media (min-width 500px) + padding 16px - @media (min-width 500px) - padding 16px + @media (min-width 600px) + padding 16px 32px + + .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 8px - right 16px + display block + margin-left auto + flex-shrink 0 font-size 0.9em - line-height 28px - - @media (min-width 500px) - top 16px & + article padding-top 8px - > .reply-to - background rgba(0, 0, 0, 0.0125) - - > .mk-note-preview - background transparent - > article - padding 14px 16px 9px 16px + padding 16px 16px 9px + + @media (min-width 600px) + padding 32px 32px 22px &:after content "" display block clear both - > .avatar-anchor + > .avatar display block float left margin 0 10px 8px 0 - position -webkit-sticky - position sticky - top 62px + width 48px + height 48px + border-radius 6px + //position -webkit-sticky + //position sticky + //top 62px @media (min-width 500px) margin-right 16px - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 6px - vertical-align bottom - - @media (min-width 500px) - width 58px - height 58px - border-radius 8px + width 58px + height 58px + border-radius 8px > .main float left @@ -336,7 +333,7 @@ export default Vue.extend({ > header display flex - align-items center + align-items baseline white-space nowrap @media (min-width 500px) @@ -347,7 +344,7 @@ export default Vue.extend({ margin 0 0.5em 0 0 padding 0 overflow hidden - color #627079 + color isDark ? #fff : #627079 font-size 1em font-weight bold text-decoration none @@ -360,122 +357,165 @@ export default Vue.extend({ margin 0 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 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 6px - color #c0c0c0 - > .created-at - color #c0c0c0 + > .visibility + margin-left 6px > .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 - [data-is-me]:after - content "you" - padding 0 4px - margin-left 4px - font-size 80% - color $theme-color-foreground - background $theme-color - border-radius 4px + &:hover + background isDark ? #707b97 : #bbc4ce - .mk-url-preview - margin-top 8px + > .content - > .channel - margin 0 + > .text + 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 + + >>> .quote + margin 8px + padding 6px 12px + color isDark ? #6f808e : #aaa + border-left solid 3px isDark ? #637182 : #eee + + > .reply + margin-right 8px + color isDark ? #99abbf : #717171 + + > .rp + margin-left 4px + font-style oblique + color #a0bf46 + + [data-is-me]:after + content "you" + padding 0 4px + margin-left 4px + font-size 80% + color $theme-color-foreground + background $theme-color + border-radius 4px + + .mk-url-preview + margin-top 8px + + > .channel + margin 0 + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% - &:before - content "" + > .media + > img display block - position absolute - top 0 - bottom 0 - left 4px - width 8px - height 8px - margin auto 0 - background #fff - border-radius 100% + max-width 100% - > .media - > img - display block - max-width 100% + > .location + margin 4px 0 + font-size 12px + color #ccc - > .location - margin 4px 0 - font-size 12px - color #ccc + > .map + width 100% + height 200px + + &:empty + display none + + > .mk-poll + font-size 80% - > .map - width 100% - height 200px + > .renote + margin 8px 0 - &:empty - display none + > .mk-note-preview + padding 16px + border dashed 1px isDark ? #4e945e : #c0dac6 + border-radius 8px > .app font-size 12px color #ccc - > .mk-poll - font-size 80% - - > .renote - margin 8px 0 - - > .mk-note-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px - > footer > button margin 0 @@ -484,14 +524,14 @@ export default Vue.extend({ border none box-shadow none font-size 1em - color #ddd + color isDark ? #606984 : #ddd cursor pointer &:not(:last-child) margin-right 28px &:hover - color #666 + color isDark ? #9198af : #666 > .count display inline @@ -505,6 +545,12 @@ export default Vue.extend({ @media (max-width 350px) display none +.note[data-darkmode] + root(true) + +.note:not([data-darkmode]) + root(false) + </style> <style lang="stylus" module> diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 573026d53e..53e232e521 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -1,30 +1,64 @@ <template> <div class="mk-notes"> + <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> + <slot name="head"></slot> - <slot></slot> - <template v-for="(note, i) in _notes"> - <mk-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="tail"></slot> + + <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> + + <div class="init" v-if="fetching"> + %fa:spinner .pulse%%i18n:common.loading% + </div> + + <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"> + <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> + <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> + <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 getNoteSummary from '../../../../../renderers/get-note-summary'; + +const displayLimit = 30; export default Vue.extend({ 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 => { @@ -36,9 +70,132 @@ 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; + }, + 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.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) { + // 親要素が display none だったら弾く + // https://github.com/syuilo/misskey/issues/1569 + // http://d.hatena.ne.jp/favril/20091105/1257403319 + if (this.$el.offsetHeight == 0) return; + + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 8) this.loadMore(); + } } } }); @@ -47,10 +204,46 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-notes - background #fff +root(isDark) + overflow hidden + background isDark ? #282C37 : #fff border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 2px rgba(#000, 0.1) + + @media (min-width 500px) + box-shadow 0 8px 32px rgba(#000, 0.1) + + .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 + text-align center + font-size 0.9em + color isDark ? #666b79 : #aaa + background isDark ? #242731 : #fdfdfd + border-bottom solid 1px isDark ? #1c2023 : #eaeaea + + span + margin 0 16px + + [data-fa] + margin-right 8px + + > .newer-indicator + position -webkit-sticky + position sticky + z-index 100 + height 3px + background $theme-color > .init padding 64px 0 @@ -73,27 +266,9 @@ export default Vue.extend({ font-size 3em color #ccc - > .date - display block - margin 0 - line-height 32px - text-align center - font-size 0.9em - color #aaa - background #fdfdfd - border-bottom solid 1px #eaeaea - - span - margin 0 16px - - [data-fa] - margin-right 8px - > footer text-align center - border-top solid 1px #eaeaea - border-bottom-left-radius 4px - border-bottom-right-radius 4px + border-top solid 1px isDark ? #1c2023 : #eaeaea &:empty display none @@ -102,10 +277,18 @@ export default Vue.extend({ margin 0 padding 16px width 100% - color $theme-color - border-radius 0 0 8px 8px + color #ccc + + @media (min-width 500px) + padding 20px &:disabled opacity 0.7 +.mk-notes[data-darkmode] + root(true) + +.mk-notes:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index 4f7c8968b2..c1b37563ce 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -1,15 +1,13 @@ <template> <div class="mk-notification"> <div class="notification reaction" v-if="notification.type == 'reaction'"> - <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="notification.user | userPage"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="text"> - <p> + <mk-avatar class="avatar" :user="notification.user"/> + <div> + <header> <mk-reaction-icon :reaction="notification.reaction"/> <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> - </p> + <mk-time :time="notification.createdAt"/> + </header> <router-link class="note-ref" :to="notification.note | notePage"> %fa:quote-left%{{ getNoteSummary(notification.note) }} %fa:quote-right% @@ -18,61 +16,55 @@ </div> <div class="notification renote" v-if="notification.type == 'renote'"> - <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="notification.user | userPage"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="text"> - <p> + <mk-avatar class="avatar" :user="notification.user"/> + <div> + <header> %fa:retweet% <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> - </p> + <mk-time :time="notification.createdAt"/> + </header> <router-link class="note-ref" :to="notification.note | notePage"> %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% </router-link> </div> </div> - <template v-if="notification.type == 'quote'"> - <mk-note :note="notification.note"/> - </template> - <div class="notification follow" v-if="notification.type == 'follow'"> - <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="notification.user | userPage"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="text"> - <p> + <mk-avatar class="avatar" :user="notification.user"/> + <div> + <header> %fa:user-plus% <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> - </p> + <mk-time :time="notification.createdAt"/> + </header> </div> </div> - <template v-if="notification.type == 'reply'"> - <mk-note :note="notification.note"/> - </template> - - <template v-if="notification.type == 'mention'"> - <mk-note :note="notification.note"/> - </template> - <div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> - <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="notification.user | userPage"> - <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - <div class="text"> - <p> + <mk-avatar class="avatar" :user="notification.user"/> + <div> + <header> %fa:chart-pie% <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> - </p> + <mk-time :time="notification.createdAt"/> + </header> <router-link class="note-ref" :to="notification.note | notePage"> %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% </router-link> </div> </div> + + <template v-if="notification.type == 'quote'"> + <mk-note :note="notification.note"/> + </template> + + <template v-if="notification.type == 'reply'"> + <mk-note :note="notification.note"/> + </template> + + <template v-if="notification.type == 'mention'"> + <mk-note :note="notification.note"/> + </template> </div> </template> @@ -91,53 +83,63 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-notification - +root(isDark) > .notification padding 16px + font-size 12px overflow-wrap break-word + @media (min-width 350px) + font-size 14px + + @media (min-width 500px) + font-size 16px + + @media (min-width 600px) + padding 24px 32px + &:after content "" display block clear both - > .mk-time - display inline - position absolute - top 16px - right 12px - vertical-align top - color rgba(0, 0, 0, 0.6) - font-size 0.9em - - > .avatar-anchor + > .avatar display block float left + width 36px + height 36px + border-radius 6px - img - min-width 36px - min-height 36px - max-width 36px - max-height 36px - border-radius 6px + @media (min-width 500px) + width 42px + height 42px - > .text + > div float right width calc(100% - 36px) padding-left 8px - p - margin 0 + @media (min-width 500px) + width calc(100% - 42px) + + > header + display flex + align-items baseline + white-space nowrap i, .mk-reaction-icon margin-right 4px + > .mk-time + margin-left auto + color isDark ? #606984 : #c0c0c0 + font-size 0.9em + > .note-preview - color rgba(0, 0, 0, 0.7) + color isDark ? #fff : #717171 > .note-ref - color rgba(0, 0, 0, 0.7) + color isDark ? #fff : #717171 [data-fa] font-size 1em @@ -147,12 +149,17 @@ export default Vue.extend({ margin-right 3px &.renote - .text p i + > div > header i color #77B255 &.follow - .text p i + > div > header i color #53c7ce -</style> +.mk-notification[data-darkmode] + root(true) +.mk-notification:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue index ad43a27b98..8ab66940c4 100644 --- a/src/client/app/mobile/views/components/notifications.vue +++ b/src/client/app/mobile/views/components/notifications.vue @@ -1,18 +1,20 @@ <template> <div class="mk-notifications"> - <div class="notifications" v-if="notifications.length != 0"> + <transition-group name="mk-notifications" class="transition notifications"> <template v-for="(notification, i) in _notifications"> <mk-notification :notification="notification" :key="notification.id"/> - <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> + <p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date"> <span>%fa:angle-up%{{ notification._datetext }}</span> <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> </p> </template> - </div> + </transition-group> + <button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template> {{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }} </button> + <p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p> <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> </div> @@ -101,28 +103,29 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-notifications - margin 8px auto - padding 0 - max-width 500px - width calc(100% - 16px) - background #fff +root(isDark) + margin 0 auto + background isDark ? #282C37 :#fff border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 2px rgba(#000, 0.1) + overflow hidden @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) + box-shadow 0 8px 32px rgba(#000, 0.1) - > .notifications + .transition + .mk-notifications-enter + .mk-notifications-leave-to + opacity 0 + transform translateY(-30px) - > .mk-notification - margin 0 auto - max-width 500px - border-bottom solid 1px rgba(0, 0, 0, 0.05) + > * + transition transform .3s ease, opacity .3s ease - &:last-child - border-bottom none + > .notifications + + > .mk-notification:not(:last-child) + border-bottom solid 1px isDark ? #1c2023 : #eaeaea > .date display block @@ -130,9 +133,9 @@ export default Vue.extend({ line-height 32px text-align center font-size 0.8em - color #aaa - background #fdfdfd - border-bottom solid 1px rgba(0, 0, 0, 0.05) + color isDark ? #666b79 : #aaa + background isDark ? #242731 : #fdfdfd + border-bottom solid 1px isDark ? #1c2023 : #eaeaea span margin 0 16px @@ -145,7 +148,7 @@ export default Vue.extend({ width 100% padding 16px color #555 - border-top solid 1px rgba(0, 0, 0, 0.05) + border-top solid 1px rgba(#000, 0.05) > [data-fa] margin-right 4px @@ -165,4 +168,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/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 861e8653ba..6d80b3046b 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -10,6 +10,11 @@ </header> <div class="form"> <mk-note-preview v-if="reply" :note="reply"/> + <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 v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : '%i18n:!@note-placeholder%'"></textarea> <div class="attaches" v-show="files.length != 0"> <x-draggable class="files" :list="files" :options="{ animation: 150 }"> @@ -20,11 +25,15 @@ </div> <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/> <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <button class="upload" @click="chooseFile">%fa:upload%</button> - <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button> - <button class="kao" @click="kao">%fa:R smile%</button> - <button class="poll" @click="poll = true">%fa:chart-pie%</button> - <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> + <footer> + <button class="upload" @click="chooseFile">%fa:upload%</button> + <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button> + <button class="kao" @click="kao">%fa:R smile%</button> + <button class="poll" @click="poll = true">%fa:chart-pie%</button> + <button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button> + <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> + <button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button> + </footer> <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/> </div> </div> @@ -33,13 +42,17 @@ <script lang="ts"> import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; +import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import getKao from '../../../common/scripts/get-kao'; export default Vue.extend({ components: { - XDraggable + XDraggable, + MkVisibilityChooser }, + props: ['reply'], + data() { return { posting: false, @@ -47,21 +60,33 @@ export default Vue.extend({ uploadings: [], files: [], poll: false, - geo: null + geo: null, + visibility: 'public', + visibleUsers: [], + useCw: false, + cw: null }; }, + mounted() { + if (this.reply && this.reply.user.host != null) { + this.text = `@${this.reply.user.username}@${this.reply.user.host} `; + } + this.$nextTick(() => { this.focus(); }); }, + methods: { focus() { (this.$refs.text as any).focus(); }, + chooseFile() { (this.$refs.file as any).click(); }, + chooseFileFromDrive() { (this as any).apis.chooseDriveFile({ multiple: true @@ -69,23 +94,29 @@ export default Vue.extend({ files.forEach(this.attachMedia); }); }, + attachMedia(driveFile) { this.files.push(driveFile); this.$emit('change-attached-media', this.files); }, + detachMedia(file) { this.files = this.files.filter(x => x.id != file.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); }, + setGeo() { if (navigator.geolocation == null) { alert('お使いの端末は位置情報に対応していません'); @@ -100,23 +131,54 @@ export default Vue.extend({ enableHighAccuracy: true }); }, + removeGeo() { this.geo = null; }, + + setVisibility() { + const w = (this as any).os.new(MkVisibilityChooser, { + source: this.$refs.visibilityButton, + compact: true, + 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); + }, + clear() { this.text = ''; this.files = []; this.poll = false; this.$emit('change-attached-media'); }, + post() { this.posting = true; - const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true; + const viaMobile = (this as any).clientSettings.disableViaMobile !== true; (this as any).api('notes/create', { text: this.text == '' ? undefined : this.text, mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, replyId: this.reply ? this.reply.id : undefined, poll: this.poll ? (this.$refs.poll as any).get() : undefined, + cw: this.useCw ? this.cw || '' : undefined, geo: this.geo ? { coordinates: [this.geo.longitude, this.geo.latitude], altitude: this.geo.altitude, @@ -125,6 +187,8 @@ export default Vue.extend({ heading: isNaN(this.geo.heading) ? null : this.geo.heading, speed: this.geo.speed, } : null, + visibility: this.visibility, + visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, viaMobile: viaMobile }).then(data => { this.$emit('note'); @@ -133,10 +197,12 @@ export default Vue.extend({ this.posting = false; }); }, + cancel() { this.$emit('cancel'); this.$destroy(); }, + kao() { this.text += getKao(); } @@ -147,29 +213,33 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-post-form +root(isDark) max-width 500px width calc(100% - 16px) margin 8px auto - background #fff + background isDark ? #282C37 : #fff border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 2px rgba(#000, 0.1) @media (min-width 500px) margin 16px auto width calc(100% - 32px) + box-shadow 0 8px 32px rgba(#000, 0.1) + + @media (min-width 600px) + margin 32px auto > header - z-index 1 + z-index 1000 height 50px - box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1) + box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1) > .cancel padding 0 width 50px line-height 50px font-size 24px - color #555 + color isDark ? #9baec8 : #555 > div position absolute @@ -203,6 +273,38 @@ export default Vue.extend({ > .mk-note-preview padding 16px + > .visibleUsers + margin-bottom 8px + font-size 14px + + > span + margin-right 16px + color isDark ? #fff : #666 + + > input + z-index 1 + + > input + > textarea + display block + padding 12px + margin 0 + width 100% + font-size 16px + color isDark ? #fff : #333 + background isDark ? #191d23 : #fff + border none + border-radius 0 + box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1) + + &:disabled + opacity 0.5 + + > textarea + max-width 100% + min-width 100% + min-height 80px + > .attaches > .files @@ -236,40 +338,30 @@ export default Vue.extend({ > .file display none - > textarea - display block - padding 12px - margin 0 - width 100% - max-width 100% - min-width 100% - min-height 80px - font-size 16px - color #333 - border none - border-bottom solid 1px #ddd - border-radius 0 + > footer + white-space nowrap + overflow auto + -webkit-overflow-scrolling touch + overflow-scrolling touch - &:disabled - opacity 0.5 + > * + display inline-block + padding 0 + margin 0 + width 48px + height 48px + font-size 20px + color #657786 + background transparent + outline none + border none + border-radius 0 + box-shadow none - > .upload - > .drive - > .kao - > .poll - > .geo - display inline-block - padding 0 - margin 0 - width 48px - height 48px - font-size 20px - color #657786 - background transparent - outline none - border none - border-radius 0 - box-shadow none +.mk-post-form[data-darkmode] + root(true) -</style> +.mk-post-form:not([data-darkmode]) + root(false) +</style> diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue index 54cc74f7f5..cc50977a58 100644 --- a/src/client/app/mobile/views/components/sub-note-content.vue +++ b/src/client/app/mobile/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 v-if="note.text" :text="note.text" :i="os.i"/> <a class="rp" v-if="note.renoteId">RP: ...</a> diff --git a/src/client/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue deleted file mode 100644 index 11b82aa456..0000000000 --- a/src/client/app/mobile/views/components/timeline.vue +++ /dev/null @@ -1,113 +0,0 @@ -<template> -<div class="mk-timeline"> - <mk-friends-maker v-if="alone"/> - <mk-notes :notes="notes"> - <div class="init" v-if="fetching"> - %fa:spinner .pulse%%i18n:common.loading% - </div> - <div class="empty" v-if="!fetching && notes.length == 0"> - %fa:R comments% - %i18n:@empty% - </div> - <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail"> - <span v-if="!moreFetching">%i18n:@load-more%</span> - <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> - </button> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -const limit = 10; - -export default Vue.extend({ - props: { - date: { - type: Date, - required: false, - default: null - } - }, - data() { - return { - fetching: true, - moreFetching: false, - notes: [], - existMore: false, - connection: null, - connectionId: null - }; - }, - computed: { - alone(): boolean { - return (this as any).os.i.followingCount == 0; - } - }, - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('note', this.onNote); - this.connection.on('follow', this.onChangeFollowing); - this.connection.on('unfollow', this.onChangeFollowing); - -this.fetch(); - }, - beforeDestroy() { - this.connection.off('note', this.onNote); - this.connection.off('follow', this.onChangeFollowing); - this.connection.off('unfollow', this.onChangeFollowing); - (this as any).os.stream.dispose(this.connectionId); - }, - methods: { - fetch(cb?) { - this.fetching = true; - (this as any).api('notes/timeline', { - limit: limit + 1, - untilDate: this.date ? (this.date as any).getTime() : undefined - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - this.notes = notes; - this.fetching = false; - this.$emit('loaded'); - if (cb) cb(); - }); - }, - more() { - this.moreFetching = true; - (this as any).api('notes/timeline', { - limit: limit + 1, - untilId: this.notes[this.notes.length - 1].id - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - this.notes = this.notes.concat(notes); - this.moreFetching = false; - }); - }, - onNote(note) { - this.notes.unshift(note); - - const isTop = window.scrollY > 8; - if (isTop) this.notes.pop(); - }, - onChangeFollowing() { - this.fetch(); - } - } -}); -</script> - -<style lang="stylus" scoped> -.mk-friends-maker - margin-bottom 8px -</style> diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue index f1b24bf2da..509463333d 100644 --- a/src/client/app/mobile/views/components/ui.header.vue +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -32,6 +32,8 @@ export default Vue.extend({ }; }, mounted() { + this.$store.commit('setUiHeaderHeight', 48); + if ((this as any).os.isSignedIn) { this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); @@ -57,9 +59,10 @@ export default Vue.extend({ } }); - 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'; @@ -141,7 +144,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.header +root(isDark) $height = 48px position fixed @@ -150,6 +153,9 @@ export default Vue.extend({ width 100% box-shadow 0 1px 0 rgba(#000, 0.075) + &, * + user-select none + > .main color rgba(#fff, 0.9) @@ -162,7 +168,7 @@ export default Vue.extend({ -webkit-backdrop-filter blur(12px) backdrop-filter blur(12px) //background-color rgba(#1b2023, 0.75) - background-color #1b2023 + background-color isDark ? #313543 : #595f6f > p display none @@ -239,4 +245,10 @@ export default Vue.extend({ line-height $height border-left solid 1px rgba(#000, 0.1) +.header[data-darkmode] + root(true) + +.header:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index 68cdacb3b5..5c65d52237 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -15,19 +15,20 @@ </router-link> <div class="links"> <ul> - <li><router-link to="/">%fa:home%%i18n:@home%%fa:angle-right%</router-link></li> - <li><router-link to="/i/notifications">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li> - <li><router-link to="/i/messaging">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li> - <li><router-link to="/othello">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li> + <li><router-link to="/" :data-active="$route.name == 'index'">%fa:home%%i18n:@home%%fa:angle-right%</router-link></li> + <li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li> + <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li> + <li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li> </ul> <ul> - <li><router-link to="/i/drive">%fa:cloud%%i18n:@drive%%fa:angle-right%</router-link></li> + <li><router-link to="/i/drive" :data-active="$route.name == 'drive'">%fa:cloud%%i18n:@drive%%fa:angle-right%</router-link></li> </ul> <ul> <li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li> </ul> <ul> <li><router-link to="/i/settings">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li> + <li @click="dark"><p><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template><span>ダークモード</span></p></li> </ul> </div> <a :href="aboutUrl"><p class="about">%i18n:@about%</p></a> @@ -113,6 +114,9 @@ export default Vue.extend({ }, onOthelloNoInvites() { this.hasGameInvitations = false; + }, + dark() { + (this as any)._updateDarkmode_(!(this as any)._darkmode_); } } }); @@ -121,7 +125,9 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.nav +root(isDark) + $color = isDark ? #c9d2e0 : #777 + .backdrop position fixed top 0 @@ -129,7 +135,7 @@ export default Vue.extend({ z-index 1025 width 100% height 100% - background rgba(0, 0, 0, 0.2) + background isDark ? rgba(#000, 0.7) : rgba(#000, 0.2) .body position fixed @@ -140,8 +146,7 @@ export default Vue.extend({ height 100% overflow auto -webkit-overflow-scrolling touch - color #777 - background #fff + background isDark ? #16191f : #fff .me display block @@ -162,7 +167,7 @@ export default Vue.extend({ left 80px padding 0 width calc(100% - 112px) - color #777 + color $color line-height 96px overflow hidden text-overflow ellipsis @@ -182,14 +187,22 @@ export default Vue.extend({ font-size 1em line-height 1em - a + a, p display block + margin 0 padding 0 20px line-height 3rem line-height calc(1rem + 30px) - color #777 + color $color text-decoration none + &[data-active] + color $theme-color-foreground + background $theme-color + + > [data-fa]:last-child + color $theme-color-foreground + > [data-fa]:first-child margin-right 0.5em @@ -205,18 +218,17 @@ export default Vue.extend({ padding 0 20px font-size 1.2em line-height calc(1rem + 30px) - color #ccc + color $color + opacity 0.5 .about margin 0 padding 1em 0 text-align center font-size 0.8em + color $color opacity 0.5 - a - color #777 - .nav-enter-active, .nav-leave-active { opacity: 1; @@ -239,4 +251,10 @@ export default Vue.extend({ opacity: 0; } +.nav[data-darkmode] + root(true) + +.nav:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue new file mode 100644 index 0000000000..59d6abbbc1 --- /dev/null +++ b/src/client/app/mobile/views/components/user-list-timeline.vue @@ -0,0 +1,93 @@ +<template> +<div> + <mk-notes ref="timeline" :more="existMore ? more : null"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { UserListStream } from '../../../common/scripts/streaming/user-list'; + +const fetchLimit = 10; + +export default Vue.extend({ + props: ['list'], + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null + }; + }, + watch: { + $route: 'init' + }, + mounted() { + this.init(); + }, + beforeDestroy() { + this.connection.close(); + }, + methods: { + init() { + if (this.connection) this.connection.close(); + this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id); + this.connection.on('note', this.onNote); + this.connection.on('userAdded', this.onUserAdded); + this.connection.on('userRemoved', this.onUserRemoved); + + this.fetch(); + }, + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('notes/user-list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + includeMyRenotes: (this as any).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/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue index 23a83b5e3a..d258360911 100644 --- a/src/client/app/mobile/views/components/user-preview.vue +++ b/src/client/app/mobile/views/components/user-preview.vue @@ -1,8 +1,6 @@ <template> <div class="mk-user-preview"> - <router-link class="avatar-anchor" :to="user | userPage"> - <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">{{ user | userName }}</router-link> @@ -40,26 +38,19 @@ export default Vue.extend({ display block clear both - > .avatar-anchor + > .avatar display block float left margin 0 10px 0 0 + width 48px + height 48px + border-radius 6px @media (min-width 500px) margin-right 16px - - > .avatar - display block - width 48px - height 48px - margin 0 - border-radius 6px - vertical-align bottom - - @media (min-width 500px) - width 58px - height 58px - border-radius 8px + width 58px + height 58px + border-radius 8px > .main float left diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue index 40b3be035e..3ceb876596 100644 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ b/src/client/app/mobile/views/components/user-timeline.vue @@ -1,17 +1,10 @@ <template> <div class="mk-user-timeline"> - <mk-notes :notes="notes"> - <div class="init" v-if="fetching"> - %fa:spinner .pulse%%i18n:common.loading% - </div> - <div class="empty" v-if="!fetching && notes.length == 0"> + <mk-notes ref="timeline" :more="existMore ? more : null"> + <div slot="empty"> %fa:R comments% {{ withMedia ? '%i18n:!@no-notes-with-media%' : '%i18n:!@no-notes%' }} </div> - <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail"> - <span v-if="!moreFetching">%i18n:@load-more%</span> - <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> - </button> </mk-notes> </div> </template> @@ -19,58 +12,68 @@ <script lang="ts"> import Vue from 'vue'; -const limit = 10; +const fetchLimit = 10; export default Vue.extend({ props: ['user', 'withMedia'], + data() { return { fetching: true, - notes: [], existMore: false, moreFetching: false }; }, + + computed: { + canFetchMore(): boolean { + return !this.moreFetching && !this.fetching && this.existMore; + } + }, + mounted() { - (this as any).api('users/notes', { - userId: this.user.id, - withMedia: this.withMedia, - limit: limit + 1 - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - this.notes = notes; - this.fetching = false; - this.$emit('loaded'); - }); + this.fetch(); }, + methods: { + fetch() { + this.fetching = true; + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('users/notes', { + userId: this.user.id, + withMedia: this.withMedia, + limit: fetchLimit + 1 + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + this.$emit('loaded'); + }, rej); + })); + }, + more() { + if (!this.canFetchMore) return; + this.moreFetching = true; (this as any).api('users/notes', { userId: this.user.id, withMedia: this.withMedia, - limit: limit + 1, - untilId: this.notes[this.notes.length - 1].id + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id }).then(notes => { - if (notes.length == limit + 1) { + if (notes.length == fetchLimit + 1) { notes.pop(); - this.existMore = true; } else { this.existMore = false; } - this.notes = this.notes.concat(notes); + notes.forEach(n => (this.$refs.timeline as any).append(n)); this.moreFetching = false; }); } } }); </script> - -<style lang="stylus" scoped> -.mk-user-timeline - max-width 600px - margin 0 auto -</style> diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue index 8fa7a9cbe6..6175067459 100644 --- a/src/client/app/mobile/views/components/users-list.vue +++ b/src/client/app/mobile/views/components/users-list.vue @@ -1,8 +1,8 @@ <template> <div class="mk-users-list"> <nav> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span> - <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span> + <span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span> + <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span> </nav> <div class="users" v-if="!fetching && users.length != 0"> <mk-user-preview v-for="u in users" :user="u" :key="u.id"/> @@ -74,7 +74,7 @@ export default Vue.extend({ justify-content center margin 0 auto max-width 600px - border-bottom solid 1px rgba(0, 0, 0, 0.2) + border-bottom solid 1px rgba(#000, 0.2) > span display block @@ -85,7 +85,7 @@ export default Vue.extend({ color #657786 border-bottom solid 2px transparent - &[data-is-active] + &[data-active] font-weight bold color $theme-color border-color $theme-color @@ -97,7 +97,7 @@ export default Vue.extend({ font-size 12px line-height 1 color #fff - background rgba(0, 0, 0, 0.3) + background rgba(#000, 0.3) border-radius 20px > .users @@ -106,14 +106,14 @@ export default Vue.extend({ width calc(100% - 16px) background #fff border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 0 1px rgba(#000, 0.2) @media (min-width 500px) margin 16px auto width calc(100% - 32px) > * - border-bottom solid 1px rgba(0, 0, 0, 0.05) + border-bottom solid 1px rgba(#000, 0.05) > .no margin 0 diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue index 7319c90849..1bdc875763 100644 --- a/src/client/app/mobile/views/components/widget-container.vue +++ b/src/client/app/mobile/views/components/widget-container.vue @@ -28,7 +28,7 @@ export default Vue.extend({ .mk-widget-container background #eee border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 0 1px rgba(#000, 0.2) overflow hidden &.hideHeader diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/dashboard.vue new file mode 100644 index 0000000000..a5ca6cb4a2 --- /dev/null +++ b/src/client/app/mobile/views/pages/dashboard.vue @@ -0,0 +1,195 @@ +<template> +<mk-ui> + <span slot="header">%fa:home%ダッシュボード</span> + <template slot="func"> + <button @click="customizing = !customizing">%fa:cog%</button> + </template> + <main> + <template v-if="customizing"> + <header> + <select v-model="widgetAdderSelected"> + <option value="profile">プロフィール</option> + <option value="calendar">カレンダー</option> + <option value="activity">アクティビティ</option> + <option value="rss">RSSリーダー</option> + <option value="photo-stream">フォトストリーム</option> + <option value="slideshow">スライドショー</option> + <option value="version">バージョン</option> + <option value="access-log">アクセスログ</option> + <option value="server">サーバー情報</option> + <option value="donation">寄付のお願い</option> + <option value="nav">ナビゲーション</option> + <option value="tips">ヒント</option> + </select> + <button @click="addWidget">追加</button> + <p><a @click="hint">カスタマイズのヒント</a></p> + </header> + <x-draggable + :list="widgets" + :options="{ handle: '.handle', animation: 150 }" + @sort="onWidgetSort" + > + <div v-for="widget in widgets" class="customize-container" :key="widget.id"> + <header> + <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> + </header> + <div @click="widgetFunc(widget.id)"> + <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> + </div> + </div> + </x-draggable> + </template> + <template v-else> + <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/> + </template> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + components: { + XDraggable + }, + data() { + return { + showNav: false, + widgets: [], + customizing: false, + widgetAdderSelected: null + }; + }, + created() { + if ((this as any).clientSettings.mobileHome == null) { + Vue.set((this as any).clientSettings, 'mobileHome', [{ + name: 'calendar', + id: 'a', data: {} + }, { + name: 'activity', + id: 'b', data: {} + }, { + name: 'rss', + id: 'c', data: {} + }, { + name: 'photo-stream', + id: 'd', data: {} + }, { + name: 'donation', + id: 'e', data: {} + }, { + name: 'nav', + id: 'f', data: {} + }, { + name: 'version', + id: 'g', data: {} + }]); + this.widgets = (this as any).clientSettings.mobileHome; + this.saveHome(); + } else { + this.widgets = (this as any).clientSettings.mobileHome; + } + + this.$watch('clientSettings', i => { + this.widgets = (this as any).clientSettings.mobileHome; + }, { + deep: true + }); + }, + + mounted() { + document.title = 'Misskey'; + }, + + methods: { + onHomeUpdated(data) { + if (data.home) { + (this as any).clientSettings.mobileHome = data.home; + this.widgets = data.home; + } else { + const w = (this as any).clientSettings.mobileHome.find(w => w.id == data.id); + if (w != null) { + w.data = data.data; + this.$refs[w.id][0].preventSave = true; + this.$refs[w.id][0].props = w.data; + this.widgets = (this as any).clientSettings.mobileHome; + } + } + }, + hint() { + alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。'); + }, + widgetFunc(id) { + const w = this.$refs[id][0]; + if (w.func) w.func(); + }, + onWidgetSort() { + this.saveHome(); + }, + addWidget() { + const widget = { + name: this.widgetAdderSelected, + id: uuid(), + data: {} + }; + + this.widgets.unshift(widget); + this.saveHome(); + }, + removeWidget(widget) { + this.widgets = this.widgets.filter(w => w.id != widget.id); + this.saveHome(); + }, + saveHome() { + (this as any).clientSettings.mobileHome = this.widgets; + (this as any).api('i/update_mobile_home', { + home: this.widgets + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +main + margin 0 auto + max-width 500px + + @media (min-width 500px) + padding 8px + + > header + padding 8px + background #fff + + .widget + margin 8px + + .customize-container + margin 8px + background #fff + + > header + line-height 32px + background #eee + + > .handle + padding 0 8px + + > .remove + position absolute + top 0 + right 0 + padding 0 8px + line-height 32px + + > div + padding 8px + + > * + pointer-events none + +</style> diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue index f3c75f71e9..33ade94e35 100644 --- a/src/client/app/mobile/views/pages/followers.vue +++ b/src/client/app/mobile/views/pages/followers.vue @@ -40,9 +40,6 @@ export default Vue.extend({ created() { this.fetch(); }, - mounted() { - document.documentElement.style.background = '#313a42'; - }, methods: { fetch() { Progress.start(); diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue index 88368ff778..c6d6d44281 100644 --- a/src/client/app/mobile/views/pages/following.vue +++ b/src/client/app/mobile/views/pages/following.vue @@ -39,9 +39,6 @@ export default Vue.extend({ created() { this.fetch(); }, - mounted() { - document.documentElement.style.background = '#313a42'; - }, methods: { fetch() { Progress.start(); diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue new file mode 100644 index 0000000000..4c1c344db1 --- /dev/null +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -0,0 +1,149 @@ +<template> +<div> + <mk-friends-maker v-if="src == 'home' && alone" style="margin-bottom:8px"/> + + <mk-notes ref="timeline" :more="existMore ? more : null"> + <div slot="empty"> + %fa:R comments% + %i18n:@empty% + </div> + </mk-notes> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +const fetchLimit = 10; + +export default Vue.extend({ + props: { + src: { + type: String, + required: true + } + }, + + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null, + connectionId: null, + unreadCount: 0, + date: null + }; + }, + + computed: { + alone(): boolean { + return (this as any).os.i.followingCount == 0; + }, + + stream(): any { + return this.src == 'home' + ? (this as any).os.stream + : this.src == 'local' + ? (this as any).os.streams.localTimelineStream + : (this as any).os.streams.globalTimelineStream; + }, + + endpoint(): string { + return this.src == 'home' + ? 'notes/timeline' + : this.src == 'local' + ? 'notes/local-timeline' + : 'notes/global-timeline'; + }, + + canFetchMore(): boolean { + return !this.moreFetching && !this.fetching && this.existMore; + } + }, + + mounted() { + this.connection = this.stream.getConnection(); + this.connectionId = this.stream.use(); + + this.connection.on('note', this.onNote); + if (this.src == 'home') { + this.connection.on('follow', this.onChangeFollowing); + this.connection.on('unfollow', this.onChangeFollowing); + } + + this.fetch(); + }, + + beforeDestroy() { + this.connection.off('note', this.onNote); + if (this.src == 'home') { + this.connection.off('follow', this.onChangeFollowing); + this.connection.off('unfollow', this.onChangeFollowing); + } + this.stream.dispose(this.connectionId); + }, + + methods: { + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api(this.endpoint, { + limit: fetchLimit + 1, + untilDate: this.date ? this.date.getTime() : undefined, + includeMyRenotes: (this as any).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.canFetchMore) return; + + this.moreFetching = true; + + (this as any).api(this.endpoint, { + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: (this as any).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); + }, + + onChangeFollowing() { + this.fetch(); + }, + + focus() { + (this.$refs.timeline as any).focus(); + }, + + warp(date) { + this.date = date; + this.fetch(); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 3d94dd7ce6..ad6d5ed408 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -1,59 +1,42 @@ <template> <mk-ui> - <span slot="header" @click="showTl = !showTl"> - <template v-if="showTl">%fa:home%%i18n:@timeline%</template> - <template v-else>%fa:home%ウィジェット</template> + <span slot="header" @click="showNav = true"> + <span> + <span v-if="src == 'home'">%fa:home%ホーム</span> + <span v-if="src == 'local'">%fa:R comments%ローカル</span> + <span v-if="src == 'global'">%fa:globe%グローバル</span> + <span v-if="src.startsWith('list')">%fa:list%{{ list.title }}</span> + </span> <span style="margin-left:8px"> - <template v-if="showTl">%fa:angle-down%</template> + <template v-if="!showNav">%fa:angle-down%</template> <template v-else>%fa:angle-up%</template> </span> </span> + <template slot="func"> - <button @click="fn" v-if="showTl">%fa:pencil-alt%</button> - <button @click="customizing = !customizing" v-else>%fa:cog%</button> + <button @click="fn">%fa:pencil-alt%</button> </template> - <main> - <div class="tl"> - <mk-timeline @loaded="onLoaded" v-show="showTl"/> + + <main :data-darkmode="_darkmode_"> + <div class="nav" v-if="showNav"> + <div class="bg" @click="showNav = false"></div> + <div class="body"> + <div> + <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> + <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span> + <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span> + <template v-if="lists"> + <span v-for="l in lists" :data-active="src == 'list:' + l.id" @click="src = 'list:' + l.id; list = l" :key="l.id">%fa:list% {{ l.title }}</span> + </template> + </div> + </div> </div> - <div class="widgets" v-show="!showTl"> - <template v-if="customizing"> - <header> - <select v-model="widgetAdderSelected"> - <option value="profile">プロフィール</option> - <option value="calendar">カレンダー</option> - <option value="activity">アクティビティ</option> - <option value="rss">RSSリーダー</option> - <option value="photo-stream">フォトストリーム</option> - <option value="slideshow">スライドショー</option> - <option value="version">バージョン</option> - <option value="access-log">アクセスログ</option> - <option value="server">サーバー情報</option> - <option value="donation">寄付のお願い</option> - <option value="nav">ナビゲーション</option> - <option value="tips">ヒント</option> - </select> - <button @click="addWidget">追加</button> - <p><a @click="hint">カスタマイズのヒント</a></p> - </header> - <x-draggable - :list="widgets" - :options="{ handle: '.handle', animation: 150 }" - @sort="onWidgetSort" - > - <div v-for="widget in widgets" class="customize-container" :key="widget.id"> - <header> - <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> - </header> - <div @click="widgetFunc(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> - </div> - </div> - </x-draggable> - </template> - <template v-else> - <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/> - </template> + + <div class="tl"> + <x-tl v-if="src == 'home'" ref="tl" key="home" src="home" @loaded="onLoaded"/> + <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> + <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> + <mk-user-list-timeline v-if="src.startsWith('list:')" ref="tl" :key="list.id" :list="list"/> </div> </main> </mk-ui> @@ -61,144 +44,58 @@ <script lang="ts"> import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import * as uuid from 'uuid'; import Progress from '../../../common/scripts/loading'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; +import XTl from './home.timeline.vue'; export default Vue.extend({ components: { - XDraggable + XTl }, + data() { return { - connection: null, - connectionId: null, - unreadCount: 0, - showTl: true, - widgets: [], - customizing: false, - widgetAdderSelected: null + src: 'home', + list: null, + lists: null, + showNav: false }; }, - created() { - if ((this as any).os.i.clientSettings.mobileHome == null) { - Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{ - name: 'calendar', - id: 'a', data: {} - }, { - name: 'activity', - id: 'b', data: {} - }, { - name: 'rss', - id: 'c', data: {} - }, { - name: 'photo-stream', - id: 'd', data: {} - }, { - name: 'donation', - id: 'e', data: {} - }, { - name: 'nav', - id: 'f', data: {} - }, { - name: 'version', - id: 'g', data: {} - }]); - this.widgets = (this as any).os.i.clientSettings.mobileHome; - this.saveHome(); - } else { - this.widgets = (this as any).os.i.clientSettings.mobileHome; + + watch: { + src() { + this.showNav = false; + }, + + showNav(v) { + if (v && this.lists === null) { + (this as any).api('users/lists/list').then(lists => { + this.lists = lists; + }); + } } + }, - this.$watch('os.i.clientSettings', i => { - this.widgets = (this as any).os.i.clientSettings.mobileHome; - }, { - deep: true - }); + created() { + if ((this as any).os.i.followingCount == 0) { + this.src = 'local'; + } }, + mounted() { document.title = 'Misskey'; - document.documentElement.style.background = '#313a42'; - - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('note', this.onStreamNote); - this.connection.on('mobile_home_updated', this.onHomeUpdated); - document.addEventListener('visibilitychange', this.onVisibilitychange, false); Progress.start(); }, - beforeDestroy() { - this.connection.off('note', this.onStreamNote); - this.connection.off('mobile_home_updated', this.onHomeUpdated); - (this as any).os.stream.dispose(this.connectionId); - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }, + methods: { fn() { (this as any).apis.post(); }, + onLoaded() { Progress.done(); }, - onStreamNote(note) { - if (document.hidden && note.userId !== (this as any).os.i.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; - } - }, - onVisibilitychange() { - if (!document.hidden) { - this.unreadCount = 0; - document.title = 'Misskey'; - } - }, - onHomeUpdated(data) { - if (data.home) { - (this as any).os.i.clientSettings.mobileHome = data.home; - this.widgets = data.home; - } else { - const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id); - if (w != null) { - w.data = data.data; - this.$refs[w.id][0].preventSave = true; - this.$refs[w.id][0].props = w.data; - this.widgets = (this as any).os.i.clientSettings.mobileHome; - } - } - }, - hint() { - alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。'); - }, - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - onWidgetSort() { - this.saveHome(); - }, - addWidget() { - const widget = { - name: this.widgetAdderSelected, - id: uuid(), - data: {} - }; - this.widgets.unshift(widget); - this.saveHome(); - }, - removeWidget(widget) { - this.widgets = this.widgets.filter(w => w.id != widget.id); - this.saveHome(); - }, - saveHome() { - (this as any).os.i.clientSettings.mobileHome = this.widgets; - (this as any).api('i/update_mobile_home', { - home: this.widgets - }); - }, warp() { } @@ -207,53 +104,74 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -main +@import '~const.styl' - > .tl - > .mk-timeline - max-width 600px +root(isDark) + > .nav + > .bg + position fixed + z-index 10000 + top 0 + left 0 + width 100% + height 100% + background rgba(#000, 0.5) + + > .body + position fixed + z-index 10001 + top 56px + left 0 + right 0 + width 300px margin 0 auto - padding 8px + background isDark ? #272f3a : #fff + border-radius 8px + box-shadow 0 0 16px rgba(#000, 0.1) - @media (min-width 500px) - padding 16px + $balloon-size = 16px - > .widgets - margin 0 auto - max-width 500px + &:after + content "" + display block + position absolute + top -($balloon-size * 2) + 1.5px + left s('calc(50% - %s)', $balloon-size) + border-top solid $balloon-size transparent + border-left solid $balloon-size transparent + border-right solid $balloon-size transparent + border-bottom solid $balloon-size isDark ? #272f3a : #fff - @media (min-width 500px) - padding 8px + > div + padding 8px 0 - > header - padding 8px - background #fff + > * + display block + padding 8px 16px + color isDark ? #cdd0d8 : #666 - .widget - margin 8px + &[data-active] + color $theme-color-foreground + background $theme-color - .customize-container - margin 8px - background #fff + &:not([data-active]):hover + background isDark ? #353e4a : #eee - > header - line-height 32px - background #eee + > .tl + max-width 680px + margin 0 auto + padding 8px - > .handle - padding 0 8px + @media (min-width 500px) + padding 16px - > .remove - position absolute - top 0 - right 0 - padding 0 8px - line-height 32px + @media (min-width 600px) + padding 32px - > div - padding 8px +main[data-darkmode] + root(true) - > * - pointer-events none +main:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue index c866be8a14..146d89d22b 100644 --- a/src/client/app/mobile/views/pages/note.vue +++ b/src/client/app/mobile/views/pages/note.vue @@ -2,11 +2,13 @@ <mk-ui> <span slot="header">%fa:R sticky-note%%i18n:@title%</span> <main v-if="!fetching"> - <a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:@next%</a> <div> <mk-note-detail :note="note"/> </div> - <a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:@prev%</a> + <footer> + <router-link v-if="note.prev" :to="note.prev">%fa:angle-left% %i18n:@prev%</router-link> + <router-link v-if="note.next" :to="note.next">%i18n:@next% %fa:angle-right%</router-link> + </footer> </main> </mk-ui> </template> @@ -30,7 +32,6 @@ export default Vue.extend({ }, mounted() { document.title = 'Misskey'; - document.documentElement.style.background = '#313a42'; }, methods: { fetch() { @@ -53,33 +54,24 @@ export default Vue.extend({ <style lang="stylus" scoped> main text-align center + padding 8px - > div - margin 8px auto - padding 0 - max-width 500px - width calc(100% - 16px) - - @media (min-width 500px) - margin 16px auto - width calc(100% - 32px) - - > a - display inline-block + @media (min-width 500px) + padding 16px - &:first-child - margin-top 8px + @media (min-width 600px) + padding 32px - @media (min-width 500px) - margin-top 16px - - &:last-child - margin-bottom 8px + > div + margin 0 auto + padding 0 + max-width 600px - @media (min-width 500px) - margin-bottom 16px + > footer + margin-top 16px - > [data-fa] - margin-right 4px + > a + display inline-block + margin 0 16px </style> diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue index cd2b633676..d0c0fe9535 100644 --- a/src/client/app/mobile/views/pages/notifications.vue +++ b/src/client/app/mobile/views/pages/notifications.vue @@ -2,7 +2,10 @@ <mk-ui> <span slot="header">%fa:R bell%%i18n:@notifications%</span> <template slot="func"><button @click="fn">%fa:check%</button></template> - <mk-notifications @fetched="onFetched"/> + + <main> + <mk-notifications @fetched="onFetched"/> + </main> </mk-ui> </template> @@ -13,7 +16,6 @@ import Progress from '../../../common/scripts/loading'; export default Vue.extend({ mounted() { document.title = 'Misskey | %i18n:@notifications%'; - document.documentElement.style.background = '#313a42'; Progress.start(); }, @@ -30,3 +32,20 @@ export default Vue.extend({ } }); </script> + +<style lang="stylus" scoped> +@import '~const.styl' + +main + width 100% + max-width 680px + margin 0 auto + padding 8px + + @media (min-width 500px) + padding 16px + + @media (min-width 600px) + padding 32px + +</style> diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue index 59da71c67d..7048cdef31 100644 --- a/src/client/app/mobile/views/pages/profile-setting.vue +++ b/src/client/app/mobile/views/pages/profile-setting.vue @@ -59,7 +59,6 @@ export default Vue.extend({ }, mounted() { document.title = 'Misskey | %i18n:@title%'; - document.documentElement.style.background = '#313a42'; }, methods: { setAvatar() { @@ -137,7 +136,7 @@ export default Vue.extend({ .form position relative background #fff - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 0 1px rgba(#000, 0.2) border-radius 8px &:before @@ -146,7 +145,7 @@ export default Vue.extend({ position absolute bottom -20px left calc(50% - 10px) - border-top solid 10px rgba(0, 0, 0, 0.2) + border-top solid 10px rgba(#000, 0.2) border-right solid 10px transparent border-bottom solid 10px transparent border-left solid 10px transparent diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue index 31035f666a..f038a6f81f 100644 --- a/src/client/app/mobile/views/pages/search.vue +++ b/src/client/app/mobile/views/pages/search.vue @@ -39,7 +39,6 @@ export default Vue.extend({ }, mounted() { document.title = `%i18n:@search%: ${this.q} | Misskey`; - document.documentElement.style.background = '#313a42'; this.fetch(); }, @@ -85,7 +84,7 @@ export default Vue.extend({ width calc(100% - 16px) background #fff border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 0 0 1px rgba(#000, 0.2) @media (min-width 500px) margin 16px auto diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue index 741559ed0b..d730e4fcff 100644 --- a/src/client/app/mobile/views/pages/selectdrive.vue +++ b/src/client/app/mobile/views/pages/selectdrive.vue @@ -62,7 +62,7 @@ export default Vue.extend({ width 100% z-index 1000 background #fff - box-shadow 0 1px rgba(0, 0, 0, 0.1) + box-shadow 0 1px rgba(#000, 0.1) > h1 margin 0 diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 8ae087749f..0e9c5ea962 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -34,7 +34,6 @@ export default Vue.extend({ }, mounted() { document.title = 'Misskey | %i18n:@settings%'; - document.documentElement.style.background = '#313a42'; }, methods: { signout() { @@ -63,7 +62,7 @@ export default Vue.extend({ width calc(100% - 32px) list-style none background #fff - border solid 1px rgba(0, 0, 0, 0.2) + border solid 1px rgba(#000, 0.2) border-radius $radius > li @@ -71,7 +70,7 @@ export default Vue.extend({ border-bottom solid 1px #ddd &:hover - background rgba(0, 0, 0, 0.1) + background rgba(#000, 0.1) &:first-child border-top-left-radius $radius diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue index 9dc07a4b86..b8245beb00 100644 --- a/src/client/app/mobile/views/pages/signup.vue +++ b/src/client/app/mobile/views/pages/signup.vue @@ -40,7 +40,7 @@ export default Vue.extend({ .form background #fff - border solid 1px rgba(0, 0, 0, 0.2) + border solid 1px rgba(#000, 0.2) border-radius 8px overflow hidden diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index 04db482df2..27482dc215 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -1,14 +1,15 @@ <template> <mk-ui> - <span slot="header" v-if="!fetching">%fa:user% {{ user | userName }}</span> - <main v-if="!fetching"> - <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> + <template slot="header" v-if="!fetching"><img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">{{ user | userName }}</template> + <main v-if="!fetching" :data-darkmode="_darkmode_"> + <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> <header> - <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div> + <div class="banner" :style="style"></div> <div class="body"> <div class="top"> <a class="avatar"> - <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> + <img :src="user.avatarUrl" alt="avatar"/> </a> <mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/> </div> @@ -44,9 +45,9 @@ </header> <nav> <div class="nav-container"> - <a :data-is-active="page == 'home'" @click="page = 'home'">%i18n:@overview%</a> - <a :data-is-active="page == 'notes'" @click="page = 'notes'">%i18n:@timeline%</a> - <a :data-is-active="page == 'media'" @click="page = 'media'">%i18n:@media%</a> + <a :data-active="page == 'home'" @click="page = 'home'">%fa:home% %i18n:@overview%</a> + <a :data-active="page == 'notes'" @click="page = 'notes'">%fa:R comment-alt% %i18n:@timeline%</a> + <a :data-active="page == 'media'" @click="page = 'media'">%fa:image% %i18n:@media%</a> </div> </nav> <div class="body"> @@ -79,6 +80,13 @@ export default Vue.extend({ computed: { age(): number { return age(this.user.profile.birthday); + }, + style(): any { + if (this.user.bannerUrl == null) return {}; + return { + backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundImage: `url(${ this.user.bannerUrl })` + }; } }, watch: { @@ -87,9 +95,6 @@ export default Vue.extend({ created() { this.fetch(); }, - mounted() { - document.documentElement.style.background = '#313a42'; - }, methods: { fetch() { Progress.start(); @@ -109,27 +114,38 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -main +root(isDark) + $bg = isDark ? #22252f : #f7f7f7 + + > .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 + max-width 600px + font-size 14px > a font-weight bold @media (max-width 500px) + padding 12px font-size 12px > header + background $bg > .banner padding-bottom 33.3% - background-color #1b1b1b + background-color isDark ? #5f7273 : #cacaca background-size cover background-position center @@ -156,13 +172,14 @@ main left -2px bottom -2px width 100% - border 3px solid #313a42 + background $bg + border 3px solid $bg border-radius 6px @media (min-width 500px) left -4px bottom -4px - border 4px solid #313a42 + border 4px solid $bg border-radius 12px > .mk-follow-button @@ -176,26 +193,26 @@ main margin 0 line-height 22px font-size 20px - color #fff + color isDark ? #fff : #757c82 > .username display inline-block line-height 20px font-size 16px font-weight bold - color #657786 + color isDark ? #657786 : #969ea5 > .followed margin-left 8px padding 2px 4px font-size 12px - color #657786 - background #f8f8f8 + color isDark ? #657786 : #fff + background isDark ? #f8f8f8 : #a7bec7 border-radius 4px > .description margin 8px 0 - color #fff + color isDark ? #fff : #757c82 > .info margin 8px 0 @@ -203,14 +220,14 @@ main > p display inline margin 0 16px 0 0 - color #a9b9c1 + color isDark ? #a9b9c1 : #90989c > i margin-right 4px > .status > a - color #657786 + color isDark ? #657786 : #818a92 &:not(:last-child) margin-right 16px @@ -218,7 +235,7 @@ main > b margin-right 4px font-size 16px - color #fff + color isDark ? #fff : #787e86 > i font-size 14px @@ -226,9 +243,9 @@ main > nav position -webkit-sticky position sticky - top 48px - box-shadow 0 4px 4px rgba(0, 0, 0, 0.3) - background-color #313a42 + top 47px + box-shadow 0 4px 4px isDark ? rgba(#000, 0.3) : rgba(#000, 0.07) + background-color $bg z-index 1 > .nav-container @@ -241,21 +258,36 @@ main display block flex 1 1 text-align center - line-height 52px - font-size 14px + line-height 48px + font-size 12px text-decoration none - color #657786 + color isDark ? #657786 : #9ca1a5 border-bottom solid 2px transparent - &[data-is-active] + @media (min-width 400px) + line-height 52px + font-size 14px + + &[data-active] font-weight bold color $theme-color border-color $theme-color > .body + max-width 680px + margin 0 auto padding 8px @media (min-width 500px) padding 16px + @media (min-width 600px) + padding 32px + +main[data-darkmode] + root(true) + +main:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue index 4ba2ffd1df..d02daf5027 100644 --- a/src/client/app/mobile/views/pages/user/home.vue +++ b/src/client/app/mobile/views/pages/user/home.vue @@ -54,30 +54,39 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.root.home +root(isDark) max-width 600px margin 0 auto > .mk-note-detail margin 0 0 8px 0 + @media (min-width 500px) + margin 0 0 16px 0 + > section - background #eee + background isDark ? #21242f : #eee border-radius 8px - box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + box-shadow 0 4px 16px rgba(#000, 0.1) &:not(:last-child) margin-bottom 8px + @media (min-width 500px) + margin-bottom 16px + > h2 margin 0 padding 8px 10px font-size 15px font-weight normal - color #465258 - background #fff + color isDark ? #b8c5cc : #465258 + background isDark ? #282c37 : #fff border-radius 8px 8px 0 0 + @media (min-width 500px) + padding 10px 16px + > i margin-right 6px @@ -89,6 +98,12 @@ export default Vue.extend({ display block margin 16px text-align center - color #cad2da + color isDark ? #cad2da : #929aa0 + +.root.home[data-darkmode] + root(true) + +.root.home:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue index 27baf8bee4..64cfa5a46c 100644 --- a/src/client/app/mobile/views/pages/welcome.vue +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -1,33 +1,33 @@ <template> <div class="welcome"> - <h1><b>Misskey</b>へようこそ</h1> - <p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p> - <div class="form"> - <p>%fa:lock% ログイン</p> - <div> - <form @submit.prevent="onSubmit"> - <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/> - <input v-model="password" type="password" placeholder="パスワード" required/> - <input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/> - <button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button> - </form> + <div> + <h1><b>Misskey</b>へようこそ</h1> + <p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p> + <div class="form"> + <p>%fa:lock% ログイン</p> <div> - <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> + <form @submit.prevent="onSubmit"> + <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/> + <input v-model="password" type="password" placeholder="パスワード" required/> + <input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/> + <button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button> + </form> + <div> + <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a> + </div> </div> </div> + <div class="tl"> + <p>%fa:comments R% タイムラインを見てみる</p> + <mk-welcome-timeline/> + </div> + <div class="users"> + <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/> + </div> + <footer> + <small>{{ copyright }}</small> + </footer> </div> - <div class="tl"> - <p>%fa:comments R% タイムラインを見てみる</p> - <mk-welcome-timeline/> - </div> - <div class="users"> - <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> - </router-link> - </div> - <footer> - <small>{{ copyright }}</small> - </footer> </div> </template> @@ -84,123 +84,116 @@ export default Vue.extend({ <style lang="stylus" scoped> .welcome - padding 16px - margin 0 auto - max-width 500px - - h1 - margin 0 - padding 8px - font-size 1.5em - font-weight normal - color #cacac3 - - & + p - margin 0 0 16px 0 - padding 0 8px 0 8px - color #949fa9 + background linear-gradient(to bottom, #1e1d65, #bd6659) - .form - margin-bottom 16px - background #fff - border solid 1px rgba(0, 0, 0, 0.2) - border-radius 8px - overflow hidden + > div + padding 16px + margin 0 auto + max-width 500px - > p + h1 margin 0 - padding 12px 20px - color #555 - background #f5f5f5 - border-bottom solid 1px #ddd + padding 8px + font-size 1.5em + font-weight normal + color #cacac3 - > div + & + p + margin 0 0 16px 0 + padding 0 8px 0 8px + color #949fa9 - > form - padding 16px + .form + margin-bottom 16px + background #fff + border solid 1px rgba(#000, 0.2) + border-radius 8px + overflow hidden + + > p + margin 0 + padding 12px 20px + color #555 + background #f5f5f5 border-bottom solid 1px #ddd - input - display block - padding 12px - margin 0 0 16px 0 - width 100% - font-size 1em - color rgba(0, 0, 0, 0.7) - background #fff - outline none - border solid 1px #ddd - border-radius 4px + > div + + > form + padding 16px + border-bottom solid 1px #ddd - button - display block - width 100% - padding 10px - margin 0 - color #333 - font-size 1em - text-align center - text-decoration none - text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) - background-image linear-gradient(#fafafa, #eaeaea) - border 1px solid #ddd - border-bottom-color #cecece - border-radius 4px + input + display block + padding 12px + margin 0 0 16px 0 + width 100% + font-size 1em + color rgba(#000, 0.7) + background #fff + outline none + border solid 1px #ddd + border-radius 4px - &:active - background-color #767676 - background-image none - border-color #444 - box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2) + button + display block + width 100% + padding 10px + margin 0 + color #333 + font-size 1em + text-align center + text-decoration none + text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) + background-image linear-gradient(#fafafa, #eaeaea) + border 1px solid #ddd + border-bottom-color #cecece + border-radius 4px - > div - padding 16px - text-align center + &:active + background-color #767676 + background-image none + border-color #444 + box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2) - > .tl - background #fff - border solid 1px rgba(0, 0, 0, 0.2) - border-radius 8px - overflow hidden + > div + padding 16px + text-align center - > p - margin 0 - padding 12px 20px - color #555 - background #f5f5f5 - border-bottom solid 1px #ddd + > .tl + background #fff + border solid 1px rgba(#000, 0.2) + border-radius 8px + overflow hidden - > .mk-welcome-timeline - max-height 300px - overflow auto + > p + margin 0 + padding 12px 20px + color #555 + background #f5f5f5 + border-bottom solid 1px #ddd - > .users - margin 12px 0 0 0 + > .mk-welcome-timeline + max-height 300px + overflow auto - > * - display inline-block - margin 4px + > .users + margin 12px 0 0 0 > * display inline-block + margin 4px width 38px height 38px - vertical-align top border-radius 6px - > footer - text-align center - color #fff - - > small - display block - margin 16px 0 0 0 - opacity 0.7 + > footer + text-align center + color #fff -</style> + > small + display block + margin 16px 0 0 0 + opacity 0.7 -<style lang="stylus"> -html -body - background linear-gradient(to bottom, #1e1d65, #bd6659) </style> diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue index 48dcafb3ed..7763be41f5 100644 --- a/src/client/app/mobile/views/widgets/activity.vue +++ b/src/client/app/mobile/views/widgets/activity.vue @@ -21,6 +21,7 @@ export default define({ methods: { func() { this.props.compact = !this.props.compact; + this.save(); } } }); diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue index 502f886ceb..59c1ec7c0e 100644 --- a/src/client/app/mobile/views/widgets/profile.vue +++ b/src/client/app/mobile/views/widgets/profile.vue @@ -34,7 +34,7 @@ export default define({ display block width 100% height 100% - background rgba(0, 0, 0, 0.5) + background rgba(#000, 0.5) .avatar display block @@ -47,7 +47,7 @@ export default define({ 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 display block @@ -58,6 +58,6 @@ export default define({ line-height 100px color #fff font-weight bold - text-shadow 0 0 8px rgba(0, 0, 0, 0.5) + text-shadow 0 0 8px rgba(#000, 0.5) </style> |