diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2018-09-15 22:15:56 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-09-15 22:15:56 +0900 |
| commit | 8f8c67ad6d33eb9ced1844bb372d517204df6c6f (patch) | |
| tree | bcaedee83bb5960a6b63291795dd95f9ed603a99 /src/client/app/desktop | |
| parent | fix mk-media darkmode (diff) | |
| parent | 8.41.0 (diff) | |
| download | misskey-8f8c67ad6d33eb9ced1844bb372d517204df6c6f.tar.gz misskey-8f8c67ad6d33eb9ced1844bb372d517204df6c6f.tar.bz2 misskey-8f8c67ad6d33eb9ced1844bb372d517204df6c6f.zip | |
Merge branch 'develop' into improve-media
Diffstat (limited to 'src/client/app/desktop')
42 files changed, 971 insertions, 715 deletions
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index f0e8a42662..e32682286c 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -6,7 +6,6 @@ import VueRouter from 'vue-router'; // Style import './style.styl'; -import '../../element.scss'; import init from '../init'; import fuckAdBlock from '../common/scripts/fuck-ad-block'; diff --git a/src/client/app/desktop/views/components/charts.vue b/src/client/app/desktop/views/components/charts.vue index c4e92e429f..6514cdf788 100644 --- a/src/client/app/desktop/views/components/charts.vue +++ b/src/client/app/desktop/views/components/charts.vue @@ -19,6 +19,11 @@ <option value="drive">%i18n:@charts.drive%</option> <option value="drive-total">%i18n:@charts.drive-total%</option> </optgroup> + <optgroup label="%i18n:@network%"> + <option value="network-requests">%i18n:@charts.network-requests%</option> + <option value="network-time">%i18n:@charts.network-time%</option> + <option value="network-usage">%i18n:@charts.network-usage%</option> + </optgroup> </select> <div> <span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span> @@ -41,7 +46,10 @@ const colors = { localPlus: 'rgb(52, 178, 118)', remotePlus: 'rgb(158, 255, 209)', localMinus: 'rgb(255, 97, 74)', - remoteMinus: 'rgb(255, 149, 134)' + remoteMinus: 'rgb(255, 149, 134)', + + incoming: 'rgb(52, 178, 118)', + outgoing: 'rgb(255, 97, 74)', }; const rgba = (color: string): string => { @@ -75,6 +83,9 @@ export default Vue.extend({ case 'drive-total': return this.driveTotalChart(); case 'drive-files': return this.driveFilesChart(); case 'drive-files-total': return this.driveFilesTotalChart(); + case 'network-requests': return this.networkRequestsChart(); + case 'network-time': return this.networkTimeChart(); + case 'network-usage': return this.networkUsageChart(); } }, @@ -544,7 +555,95 @@ export default Vue.extend({ } } }]; - } + }, + + networkRequestsChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + requests: x.network.requests + })); + + return [{ + datasets: [{ + label: 'Requests', + fill: true, + backgroundColor: rgba(colors.localPlus), + borderColor: colors.localPlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.requests })) + }] + }]; + }, + + networkTimeChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + time: x.network.requests != 0 ? (x.network.totalTime / x.network.requests) : 0, + })); + + return [{ + datasets: [{ + label: 'Avg time (ms)', + fill: true, + backgroundColor: rgba(colors.localPlus), + borderColor: colors.localPlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.time })) + }] + }]; + }, + + networkUsageChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + incoming: x.network.incomingBytes, + outgoing: x.network.outgoingBytes + })); + + return [{ + datasets: [{ + label: 'Incoming', + fill: true, + backgroundColor: rgba(colors.incoming), + borderColor: colors.incoming, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.incoming })) + }, { + label: 'Outgoing', + fill: true, + backgroundColor: rgba(colors.outgoing), + borderColor: colors.outgoing, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.outgoing })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('bytes')(value, 1); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; + } + } + } + }]; + }, } }); </script> diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue index afb6838eb6..49aeac143f 100644 --- a/src/client/app/desktop/views/components/context-menu.vue +++ b/src/client/app/desktop/views/components/context-menu.vue @@ -64,7 +64,7 @@ export default Vue.extend({ }); this.$emit('closed'); - this.$destroy(); + this.destroyDom(); } } }); diff --git a/src/client/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue index aff21c1754..bbb1e0030c 100644 --- a/src/client/app/desktop/views/components/dialog.vue +++ b/src/client/app/desktop/views/components/dialog.vue @@ -78,7 +78,7 @@ export default Vue.extend({ scale: 0.8, duration: 300, easing: [ 0.5, -0.5, 1, 0.5 ], - complete: () => this.$destroy() + complete: () => this.destroyDom() }); }, onBgClick() { diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue index 7dfd9e4359..4e8a212b00 100644 --- a/src/client/app/desktop/views/components/friends-maker.vue +++ b/src/client/app/desktop/views/components/friends-maker.vue @@ -14,7 +14,7 @@ <p class="empty" v-if="!fetching && users.length == 0">%i18n:@empty%</p> <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p> <a class="refresh" @click="refresh">%i18n:@refresh%</a> - <button class="close" @click="$destroy()" title="%i18n:@close%">%fa:times%</button> + <button class="close" @click="destroyDom()" title="%i18n:@close%">%fa:times%</button> </div> </template> diff --git a/src/client/app/desktop/views/components/media-image-dialog.vue b/src/client/app/desktop/views/components/media-image-dialog.vue index 026522d907..89a340d3ae 100644 --- a/src/client/app/desktop/views/components/media-image-dialog.vue +++ b/src/client/app/desktop/views/components/media-image-dialog.vue @@ -26,7 +26,7 @@ export default Vue.extend({ opacity: 0, duration: 100, easing: 'linear', - complete: () => this.$destroy() + complete: () => this.destroyDom() }); } } diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue index 904dc7f832..3cff8cfc04 100644 --- a/src/client/app/desktop/views/components/media-image.vue +++ b/src/client/app/desktop/views/components/media-image.vue @@ -1,5 +1,5 @@ <template> -<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false"> +<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> <div> <b>%fa:exclamation-triangle% %i18n:@sensitive%</b> <span>%i18n:@click-to-show%</span> diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue index 959cefa42c..03c93c8939 100644 --- a/src/client/app/desktop/views/components/media-video-dialog.vue +++ b/src/client/app/desktop/views/components/media-video-dialog.vue @@ -28,7 +28,7 @@ export default Vue.extend({ opacity: 0, duration: 100, easing: 'linear', - complete: () => this.$destroy() + complete: () => this.destroyDom() }); } } diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index 1ba4a9a447..7307eeb7dc 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -37,20 +37,26 @@ </router-link> </header> <div class="body"> - <div class="text"> - <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span> - <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span> - <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> - </div> - <div class="media" v-if="p.media.length > 0"> - <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" :detail="true"/> - <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> - <div class="map" v-if="p.geo" ref="map"></div> - <div class="renote" v-if="p.renote"> - <mk-note-preview :note="p.renote"/> + <p v-if="p.cw != null" class="cw"> + <span class="text" v-if="p.cw != ''">{{ p.cw }}</span> + <mk-cw-button v-model="showContent"/> + </p> + <div class="content" v-show="p.cw == null || showContent"> + <div class="text"> + <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span> + <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span> + <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> + </div> + <div class="files" v-if="p.files.length > 0"> + <mk-media-list :media-list="p.files" :raw="true"/> + </div> + <mk-poll v-if="p.poll" :note="p"/> + <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</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> <footer> @@ -86,6 +92,7 @@ import MkRenoteFormWindow from './renote-form-window.vue'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import XSub from './notes.note.sub.vue'; +import { sum } from '../../../../../prelude/array'; export default Vue.extend({ components: { @@ -104,6 +111,7 @@ export default Vue.extend({ data() { return { + showContent: false, conversation: [], conversationFetching: false, replies: [] @@ -114,22 +122,24 @@ export default Vue.extend({ isRenote(): boolean { return (this.note.renote && this.note.text == null && - this.note.mediaIds.length == 0 && + this.note.fileIds.length == 0 && this.note.poll == null); }, + p(): any { return this.isRenote ? this.note.renote : this.note; }, + reactionsCount(): number { return this.p.reactionCounts - ? Object.keys(this.p.reactionCounts) - .map(key => this.p.reactionCounts[key]) - .reduce((a, b) => a + b) + ? sum(Object.values(this.p.reactionCounts)) : 0; }, + title(): string { return new Date(this.p.createdAt).toLocaleString(); }, + urls(): string[] { if (this.p.text) { const ast = parse(this.p.text); @@ -184,22 +194,26 @@ export default Vue.extend({ this.conversation = conversation.reverse(); }); }, + reply() { (this as any).os.new(MkPostFormWindow, { reply: this.p }); }, + renote() { (this as any).os.new(MkRenoteFormWindow, { note: this.p }); }, + react() { (this as any).os.new(MkReactionPicker, { source: this.$refs.reactButton, note: this.p }); }, + menu() { (this as any).os.new(MkNoteMenu, { source: this.$refs.menuButton, @@ -327,37 +341,49 @@ root(isDark) > .body padding 8px 0 - > .text + > .cw cursor default display block margin 0 padding 0 overflow-wrap break-word - font-size 1.5em color isDark ? #fff : #717171 - > .renote - margin 8px 0 + > .text + margin-right 8px + + > .content + > .text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.5em + color isDark ? #fff : #717171 + + > .renote + margin 8px 0 - > .mk-note-preview - padding 16px - border dashed 1px #c0dac6 - border-radius 8px + > * + padding 16px + border dashed 1px #c0dac6 + border-radius 8px - > .location - margin 4px 0 - font-size 12px - color #ccc + > .location + margin 4px 0 + font-size 12px + color #ccc - > .map - width 100% - height 300px + > .map + width 100% + height 300px - &:empty - display none + &:empty + display none - > .mk-url-preview - margin-top 8px + > .mk-url-preview + margin-top 8px > footer font-size 1.2em diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue index c723db98c0..6c84165356 100644 --- a/src/client/app/desktop/views/components/note-preview.vue +++ b/src/client/app/desktop/views/components/note-preview.vue @@ -1,10 +1,16 @@ <template> -<div class="mk-note-preview" :title="title"> +<div class="qiziqtywpuaucsgarwajitwaakggnisj" :title="title"> <mk-avatar class="avatar" :user="note.user" v-if="!mini"/> <div class="main"> <mk-note-header class="header" :note="note" :mini="true"/> <div class="body"> - <mk-sub-note-content class="text" :note="note"/> + <p v-if="note.cw != null" class="cw"> + <span class="text" v-if="note.cw != ''">{{ note.cw }}</span> + <mk-cw-button v-model="showContent"/> + </p> + <div class="content" v-show="note.cw == null || showContent"> + <mk-sub-note-content class="text" :note="note"/> + </div> </div> </div> </div> @@ -25,6 +31,13 @@ export default Vue.extend({ default: false } }, + + data() { + return { + showContent: false + }; + }, + computed: { title(): string { return new Date(this.note.createdAt).toLocaleString(); @@ -52,16 +65,28 @@ root(isDark) > .body - > .text + > .cw cursor default + display block margin 0 padding 0 - color isDark ? #959ba7 : #717171 + overflow-wrap break-word + color isDark ? #fff : #717171 + + > .text + margin-right 8px + + > .content + > .text + cursor default + margin 0 + padding 0 + color isDark ? #959ba7 : #717171 -.mk-note-preview[data-darkmode] +.qiziqtywpuaucsgarwajitwaakggnisj[data-darkmode] root(true) -.mk-note-preview:not([data-darkmode]) +.qiziqtywpuaucsgarwajitwaakggnisj:not([data-darkmode]) root(false) </style> diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue index fc851e83e9..8f01ddd43c 100644 --- a/src/client/app/desktop/views/components/notes.note.sub.vue +++ b/src/client/app/desktop/views/components/notes.note.sub.vue @@ -1,10 +1,16 @@ <template> -<div class="sub" :title="title"> +<div class="tkfdzaxtkdeianobciwadajxzbddorql" :title="title"> <mk-avatar class="avatar" :user="note.user"/> <div class="main"> <mk-note-header class="header" :note="note"/> <div class="body"> - <mk-sub-note-content class="text" :note="note"/> + <p v-if="note.cw != null" class="cw"> + <span class="text" v-if="note.cw != ''">{{ note.cw }}</span> + <mk-cw-button v-model="showContent"/> + </p> + <div class="content" v-show="note.cw == null || showContent"> + <mk-sub-note-content class="text" :note="note"/> + </div> </div> </div> </div> @@ -14,7 +20,19 @@ import Vue from 'vue'; export default Vue.extend({ - props: ['note'], + props: { + note: { + type: Object, + required: true + } + }, + + data() { + return { + showContent: false + }; + }, + computed: { title(): string { return new Date(this.note.createdAt).toLocaleString(); @@ -48,20 +66,32 @@ root(isDark) > .body - > .text + > .cw cursor default + display block margin 0 padding 0 - color isDark ? #959ba7 : #717171 + overflow-wrap break-word + color isDark ? #fff : #717171 + + > .text + margin-right 8px + + > .content + > .text + cursor default + margin 0 + padding 0 + color isDark ? #959ba7 : #717171 - pre - max-height 120px - font-size 80% + pre + max-height 120px + font-size 80% -.sub[data-darkmode] +.tkfdzaxtkdeianobciwadajxzbddorql[data-darkmode] root(true) -.sub:not([data-darkmode]) +.tkfdzaxtkdeianobciwadajxzbddorql:not([data-darkmode]) root(false) </style> diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 7592ae3905..46a866f9a7 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -18,7 +18,7 @@ <div class="body"> <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 ? '%i18n:@hide%' : '%i18n:@see-more%' }}</span> + <mk-cw-button v-model="showContent"/> </p> <div class="content" v-show="p.cw == null || showContent"> <div class="text"> @@ -28,15 +28,13 @@ <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/> <a class="rp" v-if="p.renote">RP:</a> </div> - <div class="media" v-if="p.media.length > 0"> - <mk-media-list :media-list="p.media"/> + <div class="files" v-if="p.files.length > 0"> + <mk-media-list :media-list="p.files"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <a class="location" v-if="p.geo" :href="`https://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 class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> </div> </div> @@ -78,6 +76,7 @@ import MkRenoteFormWindow from './renote-form-window.vue'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import XSub from './notes.note.sub.vue'; +import { sum } from '../../../../../prelude/array'; function focus(el, fn) { const target = fn(el); @@ -95,7 +94,12 @@ export default Vue.extend({ XSub }, - props: ['note'], + props: { + note: { + type: Object, + required: true + } + }, data() { return { @@ -110,7 +114,7 @@ export default Vue.extend({ isRenote(): boolean { return (this.note.renote && this.note.text == null && - this.note.mediaIds.length == 0 && + this.note.fileIds.length == 0 && this.note.poll == null); }, @@ -120,9 +124,7 @@ export default Vue.extend({ reactionsCount(): number { return this.p.reactionCounts - ? Object.keys(this.p.reactionCounts) - .map(key => this.p.reactionCounts[key]) - .reduce((a, b) => a + b) + ? sum(Object.values(this.p.reactionCounts)) : 0; }, @@ -399,19 +401,6 @@ root(isDark) > .text margin-right 8px - > .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 - - &:hover - background isDark ? #707b97 : #bbc4ce - > .content > .text @@ -470,7 +459,7 @@ root(isDark) > .renote margin 8px 0 - > .mk-note-preview + > * padding 16px border dashed 1px isDark ? #4e945e : #c0dac6 border-radius 8px diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index a1c1207a7b..26aa0c8dea 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -10,8 +10,7 @@ </div> <!-- トランジションを有効にするとなぜかメモリリークする --> - <!--<transition-group name="mk-notes" class="transition">--> - <div class="notes"> + <transition-group name="mk-notes" class="notes transition" tag="div"> <template v-for="(note, i) in _notes"> <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> @@ -19,8 +18,7 @@ <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span> </p> </template> - </div> - <!--</transition-group>--> + </transition-group> <footer v-if="more"> <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> @@ -122,7 +120,7 @@ export default Vue.extend({ prepend(note, silent = false) { //#region 弾く const isMyNote = note.userId == this.$store.state.i.id; - const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; if (this.$store.state.settings.showMyRenotes === false) { if (isMyNote && isPureRenote) { diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index bfe71903e4..9d0e73adef 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -2,8 +2,7 @@ <div class="mk-notifications"> <div class="notifications" v-if="notifications.length != 0"> <!-- トランジションを有効にするとなぜかメモリリークする --> - <!-- <transition-group name="mk-notifications" class="transition"> --> - <div> + <transition-group name="mk-notifications" class="transition" tag="div"> <template v-for="(notification, i) in _notifications"> <div class="notification" :class="notification.type" :key="notification.id"> <mk-time :time="notification.createdAt"/> @@ -97,8 +96,7 @@ <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span> </p> </template> - </div> - <!-- </transition-group> --> + </transition-group> </div> <button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications"> <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }} diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue index 51a416e281..a88c96d1bf 100644 --- a/src/client/app/desktop/views/components/post-form-window.vue +++ b/src/client/app/desktop/views/components/post-form-window.vue @@ -4,7 +4,7 @@ <span class="icon" v-if="geo">%fa:map-marker-alt%</span> <span v-if="!reply">%i18n:@note%</span> <span v-if="reply">%i18n:@reply%</span> - <span class="count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span> + <span class="count" v-if="files.length != 0">{{ '%i18n:@attaches%'.replace('{}', files.length) }}</span> <span class="count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span> </span> @@ -14,7 +14,7 @@ :reply="reply" @posted="onPosted" @change-uploadings="onChangeUploadings" - @change-attached-media="onChangeMedia" + @change-attached-files="onChangeFiles" @geo-attached="onGeoAttached" @geo-dettached="onGeoDettached"/> </div> @@ -29,7 +29,7 @@ export default Vue.extend({ data() { return { uploadings: [], - media: [], + files: [], geo: null }; }, @@ -42,8 +42,8 @@ export default Vue.extend({ onChangeUploadings(files) { this.uploadings = files; }, - onChangeMedia(media) { - this.media = media; + onChangeFiles(files) { + this.files = files; }, onGeoAttached(geo) { this.geo = geo; diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index 2ca5484610..c371940aa3 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -20,7 +20,7 @@ @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" v-autocomplete="'text'" ></textarea> - <div class="medias" :class="{ with: poll }" v-show="files.length != 0"> + <div class="files" :class="{ with: poll }" v-show="files.length != 0"> <x-draggable :list="files" :options="{ animation: 150 }"> <div v-for="file in files" :key="file.id"> <div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div> @@ -45,7 +45,7 @@ <span v-if="visibility === 'specified'">%fa:envelope%</span> <span v-if="visibility === 'private'">%fa:lock%</span> </button> - <p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p> + <p class="text-count" :class="{ over: this.trimmedLength(text) > 1000 }">{{ 1000 - this.trimmedLength(text) }}</p> <button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> {{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/> </button> @@ -62,6 +62,9 @@ import getFace from '../../../common/scripts/get-face'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; +import { erase, unique } from '../../../../../prelude/array'; +import { length } from 'stringz'; +import parseAcct from '../../../../../misc/acct/parse'; export default Vue.extend({ components: { @@ -99,7 +102,7 @@ export default Vue.extend({ useCw: false, cw: null, geo: null, - visibility: this.$store.state.device.visibility || 'public', + visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility, visibleUsers: [], autocomplete: null, draghover: false, @@ -145,7 +148,7 @@ export default Vue.extend({ canPost(): boolean { return !this.posting && (1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && - (this.text.trim().length <= 1000); + (length(this.text.trim()) <= 1000); } }, @@ -188,7 +191,7 @@ export default Vue.extend({ (this.$refs.poll as any).set(draft.data.poll); }); } - this.$emit('change-attached-media', this.files); + this.$emit('change-attached-files', this.files); } } @@ -197,6 +200,10 @@ export default Vue.extend({ }, methods: { + trimmedLength(text: string) { + return length(text.trim()); + }, + addTag(tag: string) { insertTextAtCursor(this.$refs.text, ` #${tag} `); }, @@ -225,12 +232,12 @@ export default Vue.extend({ attachMedia(driveFile) { this.files.push(driveFile); - this.$emit('change-attached-media', this.files); + this.$emit('change-attached-files', this.files); }, detachMedia(id) { this.files = this.files.filter(x => x.id != id); - this.$emit('change-attached-media', this.files); + this.$emit('change-attached-files', this.files); }, onChangeFile() { @@ -249,7 +256,7 @@ export default Vue.extend({ this.text = ''; this.files = []; this.poll = false; - this.$emit('change-attached-media', this.files); + this.$emit('change-attached-files', this.files); }, onKeydown(e) { @@ -297,7 +304,7 @@ export default Vue.extend({ if (driveFile != null && driveFile != '') { const file = JSON.parse(driveFile); this.files.push(file); - this.$emit('change-attached-media', this.files); + this.$emit('change-attached-files', this.files); e.preventDefault(); } //#endregion @@ -336,17 +343,16 @@ export default Vue.extend({ addVisibleUser() { (this as any).apis.input({ title: '%i18n:@enter-username%' - }).then(username => { - (this as any).api('users/show', { - username - }).then(user => { + }).then(acct => { + if (acct.startsWith('@')) acct = acct.substr(1); + (this as any).api('users/show', parseAcct(acct)).then(user => { this.visibleUsers.push(user); }); }); }, removeVisibleUser(user) { - this.visibleUsers = this.visibleUsers.filter(u => u != user); + this.visibleUsers = erase(user, this.visibleUsers); }, post() { @@ -354,7 +360,7 @@ export default Vue.extend({ (this as any).api('notes/create', { text: this.text == '' ? undefined : this.text, - mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, + fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, replyId: this.reply ? this.reply.id : undefined, renoteId: this.renote ? this.renote.id : undefined, poll: this.poll ? (this.$refs.poll as any).get() : undefined, @@ -391,7 +397,7 @@ export default Vue.extend({ if (this.text && this.text != '') { const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag); const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; - localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], []))); + localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); } }, @@ -514,7 +520,7 @@ root(isDark) margin-right 8px white-space nowrap - > .medias + > .files margin 0 padding 0 background isDark ? #181b23 : lighten($theme-color, 98%) diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue index 38eab3362f..c5192ecaac 100644 --- a/src/client/app/desktop/views/components/renote-form.vue +++ b/src/client/app/desktop/views/components/renote-form.vue @@ -1,6 +1,6 @@ <template> <div class="mk-renote-form"> - <mk-note-preview :note="note"/> + <mk-note-preview class="preview" :note="note"/> <template v-if="!quote"> <footer> <a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a> @@ -61,7 +61,7 @@ export default Vue.extend({ root(isDark) - > .mk-note-preview + > .preview margin 16px 22px > footer diff --git a/src/client/app/desktop/views/components/settings.drive.vue b/src/client/app/desktop/views/components/settings.drive.vue index e8a3cc9685..d254b27110 100644 --- a/src/client/app/desktop/views/components/settings.drive.vue +++ b/src/client/app/desktop/views/components/settings.drive.vue @@ -1,7 +1,6 @@ <template> <div class="root"> <template v-if="!fetching"> - <el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor((usage / capacity) * 100)"/> <p><b>{{ capacity | bytes }}</b>%i18n:max%<b>{{ usage | bytes }}</b>%i18n:in-use%</p> </template> </div> diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue index 262583b640..d47b5b224b 100644 --- a/src/client/app/desktop/views/components/settings.profile.vue +++ b/src/client/app/desktop/views/components/settings.profile.vue @@ -19,7 +19,7 @@ </label> <label class="ui from group"> <p>%i18n:@birthday%</p> - <el-date-picker v-model="birthday" type="date" value-format="yyyy-MM-dd"/> + <input type="date" v-model="birthday"/> </label> <button class="ui primary" @click="save">%i18n:@save%</button> <section> @@ -30,6 +30,7 @@ <h2>%i18n:@other%</h2> <mk-switch v-model="$store.state.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/> <mk-switch v-model="$store.state.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/> + <mk-switch v-model="alwaysMarkNsfw" text="%i18n:common.always-mark-nsfw%"/> </section> </div> </template> @@ -46,6 +47,12 @@ export default Vue.extend({ birthday: null, }; }, + computed: { + alwaysMarkNsfw: { + get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, + set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); } + }, + }, created() { this.name = this.$store.state.i.name || ''; this.location = this.$store.state.i.profile.location; diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 7d6f1d55fb..709715e598 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -20,12 +20,28 @@ <section class="web" v-show="page == 'web'"> <h1>%i18n:@behaviour%</h1> - <mk-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="%i18n:@fetch-on-scroll%"> + <mk-switch v-model="fetchOnScroll" text="%i18n:@fetch-on-scroll%"> <span>%i18n:@fetch-on-scroll-desc%</span> </mk-switch> <mk-switch v-model="autoPopout" text="%i18n:@auto-popout%"> <span>%i18n:@auto-popout-desc%</span> </mk-switch> + + <section> + <header>%i18n:@note-visibility%</header> + <mk-switch v-model="rememberNoteVisibility" text="%i18n:@remember-note-visibility%"/> + <section> + <header>%i18n:@default-note-visibility%</header> + <ui-select v-model="defaultNoteVisibility"> + <option value="public">%i18n:common.note-visibility.public%</option> + <option value="home">%i18n:common.note-visibility.home%</option> + <option value="followers">%i18n:common.note-visibility.followers%</option> + <option value="specified">%i18n:common.note-visibility.specified%</option> + <option value="private">%i18n:common.note-visibility.private%</option> + </ui-select> + </section> + </section> + <details> <summary>%i18n:@advanced%</summary> <mk-switch v-model="apiViaStream" text="%i18n:@api-via-stream%"> @@ -43,23 +59,26 @@ <button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button> <button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button> <mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/> - <mk-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons" text="%i18n:@circle-icons%"/> - <mk-switch v-model="$store.state.settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="%i18n:@gradient-window-header%"/> - <mk-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi" text="%i18n:common.i-like-sushi%"/> + <mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/> + <mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/> + <mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/> + <mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/> + <mk-switch v-model="iLikeSushi" text="%i18n:common.i-like-sushi%"/> </div> - <mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> - <mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> - <mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/> - <mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/> - <mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/> - <mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> - <mk-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes" text="%i18n:@show-local-renotes%"/> - <mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%"> + <mk-switch v-model="showPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> + <mk-switch v-model="suggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> + <mk-switch v-model="showClockOnHeader" text="%i18n:@show-clock-on-header%"/> + <mk-switch v-model="alwaysShowNsfw" text="%i18n:common.always-show-nsfw%"/> + <mk-switch v-model="showReplyTarget" text="%i18n:@show-reply-target%"/> + <mk-switch v-model="showMyRenotes" text="%i18n:@show-my-renotes%"/> + <mk-switch v-model="showRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> + <mk-switch v-model="showLocalRenotes" text="%i18n:@show-local-renotes%"/> + <mk-switch v-model="showMaps" text="%i18n:@show-maps%"> <span>%i18n:@show-maps-desc%</span> </mk-switch> - <mk-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/> - <mk-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels" text="%i18n:common.show-reversi-board-labels%"/> - <mk-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones" text="%i18n:common.use-contrast-reversi-stones%"/> + <mk-switch v-model="disableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/> + <mk-switch v-model="games_reversi_showBoardLabels" text="%i18n:common.show-reversi-board-labels%"/> + <mk-switch v-model="games_reversi_useContrastStones" text="%i18n:common.use-contrast-reversi-stones%"/> </section> <section class="web" v-show="page == 'web'"> @@ -68,32 +87,31 @@ <span>%i18n:@enable-sounds-desc%</span> </mk-switch> <label>%i18n:@volume%</label> - <el-slider + <input type="range" v-model="soundVolume" - :show-input="true" - :format-tooltip="v => `${v * 100}%`" :disabled="!enableSounds" - :max="1" - :step="0.1" + max="1" + step="0.1" /> <button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button> </section> <section class="web" v-show="page == 'web'"> <h1>%i18n:@mobile%</h1> - <mk-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile" text="%i18n:@disable-via-mobile%"/> + <mk-switch v-model="disableViaMobile" text="%i18n:@disable-via-mobile%"/> </section> <section class="web" v-show="page == 'web'"> <h1>%i18n:@language%</h1> - <el-select v-model="lang" placeholder="%i18n:@pick-language%"> - <el-option-group label="%i18n:@recommended%"> - <el-option label="%i18n:@auto%" :value="null"/> - </el-option-group> - <el-option-group label="%i18n:@specify-language%"> - <el-option v-for="x in langs" :label="x[1]" :value="x[0]" :key="x[0]"/> - </el-option-group> - </el-select> + <select v-model="lang" placeholder="%i18n:@pick-language%"> + <optgroup label="%i18n:@recommended%"> + <option value="">%i18n:@auto%</option> + </optgroup> + + <optgroup label="%i18n:@specify-language%"> + <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> + </optgroup> + </select> <div class="none ui info"> <p>%fa:info-circle%%i18n:@language-desc%</p> </div> @@ -188,10 +206,6 @@ <mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%"> <span>%i18n:@experimental-desc%</span> </mk-switch> - <details v-if="debug"> - <summary>%i18n:@tools%</summary> - <button class="ui button block" @click="taskmngr">%i18n:@task-manager%</button> - </details> </section> </div> </div> @@ -209,7 +223,6 @@ import XSignins from './settings.signins.vue'; import XDrive from './settings.drive.vue'; import { url, langs, version } from '../../../config'; import checkForUpdate from '../../../common/scripts/check-for-update'; -import MkTaskManager from './taskmanager.vue'; export default Vue.extend({ components: { @@ -276,7 +289,112 @@ export default Vue.extend({ enableExperimentalFeatures: { get() { return this.$store.state.device.enableExperimentalFeatures; }, set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); } - } + }, + + alwaysShowNsfw: { + get() { return this.$store.state.device.alwaysShowNsfw; }, + set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); } + }, + + fetchOnScroll: { + get() { return this.$store.state.settings.fetchOnScroll; }, + set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); } + }, + + rememberNoteVisibility: { + get() { return this.$store.state.settings.rememberNoteVisibility; }, + set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); } + }, + + defaultNoteVisibility: { + get() { return this.$store.state.settings.defaultNoteVisibility; }, + set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); } + }, + + showReplyTarget: { + get() { return this.$store.state.settings.showReplyTarget; }, + set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); } + }, + + showMyRenotes: { + get() { return this.$store.state.settings.showMyRenotes; }, + set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); } + }, + + showRenotedMyNotes: { + get() { return this.$store.state.settings.showRenotedMyNotes; }, + set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); } + }, + + showLocalRenotes: { + get() { return this.$store.state.settings.showLocalRenotes; }, + set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); } + }, + + showPostFormOnTopOfTl: { + get() { return this.$store.state.settings.showPostFormOnTopOfTl; }, + set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); } + }, + + suggestRecentHashtags: { + get() { return this.$store.state.settings.suggestRecentHashtags; }, + set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); } + }, + + showClockOnHeader: { + get() { return this.$store.state.settings.showClockOnHeader; }, + set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); } + }, + + showMaps: { + get() { return this.$store.state.settings.showMaps; }, + set(value) { this.$store.dispatch('settings/set', { key: 'showMaps', value }); } + }, + + circleIcons: { + get() { return this.$store.state.settings.circleIcons; }, + set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); } + }, + + contrastedAcct: { + get() { return this.$store.state.settings.contrastedAcct; }, + set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); } + }, + + showFullAcct: { + get() { return this.$store.state.settings.showFullAcct; }, + set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); } + }, + + iLikeSushi: { + get() { return this.$store.state.settings.iLikeSushi; }, + set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); } + }, + + games_reversi_showBoardLabels: { + get() { return this.$store.state.settings.games.reversi.showBoardLabels; }, + set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); } + }, + + games_reversi_useContrastStones: { + get() { return this.$store.state.settings.games.reversi.useContrastStones; }, + set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); } + }, + + disableAnimatedMfm: { + get() { return this.$store.state.settings.disableAnimatedMfm; }, + set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); } + }, + + disableViaMobile: { + get() { return this.$store.state.settings.disableViaMobile; }, + set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); } + }, + + gradientWindowHeader: { + get() { return this.$store.state.settings.gradientWindowHeader; }, + set(value) { this.$store.dispatch('settings/set', { key: 'gradientWindowHeader', value }); } + }, }, created() { (this as any).os.getMeta().then(meta => { @@ -284,9 +402,6 @@ export default Vue.extend({ }); }, methods: { - taskmngr() { - (this as any).os.new(MkTaskManager); - }, customizeHome() { this.$router.push('/i/customize-home'); this.$emit('done'); @@ -305,113 +420,11 @@ export default Vue.extend({ wallpaperId: null }); }, - onChangeFetchOnScroll(v) { - this.$store.dispatch('settings/set', { - key: 'fetchOnScroll', - value: v - }); - }, onChangeAutoWatch(v) { (this as any).api('i/update', { autoWatch: v }); }, - onChangeDark(v) { - this.$store.dispatch('settings/set', { - key: 'dark', - value: v - }); - }, - onChangeShowPostFormOnTopOfTl(v) { - this.$store.dispatch('settings/set', { - key: 'showPostFormOnTopOfTl', - value: v - }); - }, - onChangeSuggestRecentHashtags(v) { - this.$store.dispatch('settings/set', { - key: 'suggestRecentHashtags', - value: v - }); - }, - onChangeShowClockOnHeader(v) { - this.$store.dispatch('settings/set', { - key: 'showClockOnHeader', - value: v - }); - }, - onChangeShowReplyTarget(v) { - this.$store.dispatch('settings/set', { - key: 'showReplyTarget', - value: v - }); - }, - onChangeShowMyRenotes(v) { - this.$store.dispatch('settings/set', { - key: 'showMyRenotes', - value: v - }); - }, - onChangeShowRenotedMyNotes(v) { - this.$store.dispatch('settings/set', { - key: 'showRenotedMyNotes', - value: v - }); - }, - onChangeShowLocalRenotes(v) { - this.$store.dispatch('settings/set', { - key: 'showLocalRenotes', - value: v - }); - }, - onChangeShowMaps(v) { - this.$store.dispatch('settings/set', { - key: 'showMaps', - value: v - }); - }, - onChangeCircleIcons(v) { - this.$store.dispatch('settings/set', { - key: 'circleIcons', - value: v - }); - }, - onChangeILikeSushi(v) { - this.$store.dispatch('settings/set', { - key: 'iLikeSushi', - value: v - }); - }, - onChangeReversiBoardLabels(v) { - this.$store.dispatch('settings/set', { - key: 'games.reversi.showBoardLabels', - value: v - }); - }, - onChangeUseContrastReversiStones(v) { - this.$store.dispatch('settings/set', { - key: 'games.reversi.useContrastStones', - value: v - }); - }, - onChangeDisableAnimatedMfm(v) { - this.$store.dispatch('settings/set', { - key: 'disableAnimatedMfm', - value: v - }); - }, - onChangeGradientWindowHeader(v) { - this.$store.dispatch('settings/set', { - key: 'gradientWindowHeader', - value: v - }); - }, - onChangeDisableViaMobile(v) { - this.$store.dispatch('settings/set', { - key: 'disableViaMobile', - value: v - }); - }, checkForUpdate() { this.checkingForUpdate = true; checkForUpdate((this as any).os, true, true).then(newer => { diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue index cb0374b910..6889dc231e 100644 --- a/src/client/app/desktop/views/components/sub-note-content.vue +++ b/src/client/app/desktop/views/components/sub-note-content.vue @@ -7,9 +7,9 @@ <misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/> <a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a> </div> - <details v-if="note.media.length > 0"> - <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary> - <mk-media-list :media-list="note.media"/> + <details v-if="note.files.length > 0"> + <summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary> + <mk-media-list :media-list="note.files"/> </details> <details v-if="note.poll"> <summary>%i18n:@poll%</summary> diff --git a/src/client/app/desktop/views/components/taskmanager.vue b/src/client/app/desktop/views/components/taskmanager.vue deleted file mode 100644 index 1f1385add8..0000000000 --- a/src/client/app/desktop/views/components/taskmanager.vue +++ /dev/null @@ -1,219 +0,0 @@ -<template> -<mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager"> - <span slot="header" :class="$style.header">%fa:stethoscope%%i18n:@title%</span> - <el-tabs :class="$style.content"> - <el-tab-pane label="Requests"> - <el-table - :data="os.requests" - style="width: 100%" - :default-sort="{prop: 'date', order: 'descending'}" - > - <el-table-column type="expand"> - <template slot-scope="props"> - <pre>{{ props.row.data }}</pre> - <pre>{{ props.row.res }}</pre> - </template> - </el-table-column> - - <el-table-column - label="Requested at" - prop="date" - sortable - > - <template slot-scope="scope"> - <b style="margin-right: 8px">{{ scope.row.date.getTime() }}</b> - <span>(<mk-time :time="scope.row.date"/>)</span> - </template> - </el-table-column> - - <el-table-column - label="Name" - > - <template slot-scope="scope"> - <b>{{ scope.row.name }}</b> - </template> - </el-table-column> - - <el-table-column - label="Status" - > - <template slot-scope="scope"> - <span>{{ scope.row.status || '(pending)' }}</span> - </template> - </el-table-column> - </el-table> - </el-tab-pane> - - <el-tab-pane label="Streams"> - <el-table - :data="os.connections" - style="width: 100%" - > - <el-table-column - label="Uptime" - > - <template slot-scope="scope"> - <mk-timer v-if="scope.row.connectedAt" :time="scope.row.connectedAt"/> - <span v-else>-</span> - </template> - </el-table-column> - - <el-table-column - label="Name" - > - <template slot-scope="scope"> - <b>{{ scope.row.name == '' ? '[Home]' : scope.row.name }}</b> - </template> - </el-table-column> - - <el-table-column - label="User" - > - <template slot-scope="scope"> - <span>{{ scope.row.user || '(anonymous)' }}</span> - </template> - </el-table-column> - - <el-table-column - prop="state" - label="State" - /> - - <el-table-column - prop="in" - label="In" - /> - - <el-table-column - prop="out" - label="Out" - /> - </el-table> - </el-tab-pane> - - <el-tab-pane label="Streams (Inspect)"> - <el-tabs type="card" style="height:50%"> - <el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab"> - <div style="padding: 12px 0 0 12px"> - <el-button size="mini" @click="send(c)">Send</el-button> - <el-button size="mini" type="warning" @click="c.isSuspended = true" v-if="!c.isSuspended">Suspend</el-button> - <el-button size="mini" type="success" @click="c.isSuspended = false" v-else>Resume</el-button> - <el-button size="mini" type="danger" @click="c.close">Disconnect</el-button> - </div> - - <el-table - :data="c.inout" - style="width: 100%" - :default-sort="{prop: 'at', order: 'descending'}" - > - <el-table-column type="expand"> - <template slot-scope="props"> - <pre>{{ props.row.data }}</pre> - </template> - </el-table-column> - - <el-table-column - label="Date" - prop="at" - sortable - > - <template slot-scope="scope"> - <b style="margin-right: 8px">{{ scope.row.at.getTime() }}</b> - <span>(<mk-time :time="scope.row.at"/>)</span> - </template> - </el-table-column> - - <el-table-column - label="Type" - > - <template slot-scope="scope"> - <span>{{ getMessageType(scope.row.data) }}</span> - </template> - </el-table-column> - - <el-table-column - label="Incoming / Outgoing" - prop="type" - /> - </el-table> - </el-tab-pane> - </el-tabs> - </el-tab-pane> - - <el-tab-pane label="Windows"> - <el-table - :data="Array.from(os.windows.windows)" - style="width: 100%" - > - <el-table-column - label="Name" - > - <template slot-scope="scope"> - <b>{{ scope.row.name || '(unknown)' }}</b> - </template> - </el-table-column> - - <el-table-column - label="Operations" - > - <template slot-scope="scope"> - <el-button size="mini" type="danger" @click="scope.row.close">Close</el-button> - </template> - </el-table-column> - </el-table> - </el-tab-pane> - </el-tabs> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - mounted() { - (this as any).os.windows.on('added', this.onWindowsChanged); - (this as any).os.windows.on('removed', this.onWindowsChanged); - }, - beforeDestroy() { - (this as any).os.windows.off('added', this.onWindowsChanged); - (this as any).os.windows.off('removed', this.onWindowsChanged); - }, - methods: { - getMessageType(data): string { - return data.type ? data.type : '-'; - }, - onWindowsChanged() { - this.$forceUpdate(); - }, - send(c) { - (this as any).apis.input({ - title: 'Send a JSON message', - allowEmpty: false - }).then(json => { - c.send(JSON.parse(json)); - }); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > [data-fa] - margin-right 4px - -.content - height 100% - overflow auto - -</style> - -<style> -.el-tabs__header { - margin-bottom: 0 !important; -} - -.el-tabs__item { - padding: 0 20px !important; -} -</style> diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index 52a7753438..8d72016f22 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -2,8 +2,8 @@ <div class="mk-timeline"> <header> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> - <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> - <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> + <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span> + <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> <button @click="chooseList" title="%i18n:@list%">%fa:list%</button> @@ -29,7 +29,8 @@ export default Vue.extend({ data() { return { src: 'home', - list: null + list: null, + enableLocalTimeline: false }; }, @@ -44,6 +45,10 @@ export default Vue.extend({ }, created() { + (this as any).os.getMeta().then(meta => { + this.enableLocalTimeline = !meta.disableLocalTimeline; + }); + if (this.$store.state.device.tl) { this.src = this.$store.state.device.tl.src; if (this.src == 'list') { diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue index 68413914c0..7519124870 100644 --- a/src/client/app/desktop/views/components/ui-notification.vue +++ b/src/client/app/desktop/views/components/ui-notification.vue @@ -27,7 +27,7 @@ export default Vue.extend({ translateY: -64, duration: 500, easing: 'easeInElastic', - complete: () => this.$destroy() + complete: () => this.destroyDom() }); }, 6000); }); diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue index 1e1755ec3c..f6d6d68a7f 100644 --- a/src/client/app/desktop/views/components/user-preview.vue +++ b/src/client/app/desktop/views/components/user-preview.vue @@ -75,7 +75,7 @@ export default Vue.extend({ 'margin-top': '-8px', duration: 200, easing: 'easeOutQuad', - complete: () => this.$destroy() + complete: () => this.destroyDom() }); } } diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue index 262fd38cd1..f42d577fce 100644 --- a/src/client/app/desktop/views/components/users-list.item.vue +++ b/src/client/app/desktop/views/components/users-list.item.vue @@ -1,17 +1,16 @@ <template> -<div class="root item"> - <mk-avatar class="avatar" :user="user"/> - <div class="main"> - <header> - <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> - <span class="username">@{{ user | acct }}</span> - </header> - <div class="body"> - <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p> - <div class="description">{{ user.description }}</div> +<div class="zvdbznxvfixtmujpsigoccczftvpiwqh"> + <div class="banner" :style="bannerStyle"></div> + <mk-avatar class="avatar" :user="user" :disable-preview="true"/> + <div class="body"> + <router-link :to="user | userPage" class="name">{{ user | userName }}</router-link> + <span class="username">@{{ user | acct }}</span> + <div class="description"> + <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/> </div> + <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p> + <mk-follow-button :user="user" :size="'big'"/> </div> - <mk-follow-button :user="user"/> </div> </template> @@ -19,76 +18,69 @@ import Vue from 'vue'; export default Vue.extend({ - props: ['user'] + props: ['user'], + + computed: { + bannerStyle(): any { + if (this.user.bannerUrl == null) return {}; + return { + backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundImage: `url(${ this.user.bannerUrl })` + }; + } + }, }); </script> <style lang="stylus" scoped> -.root.item - padding 16px +.zvdbznxvfixtmujpsigoccczftvpiwqh + $bg = #fff + + margin 16px auto + max-width calc(100% - 32px) font-size 16px + text-align center + background $bg + box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) - &:after - content "" - display block - clear both + > .banner + height 100px + background-color #f9f4f4 + background-position center + background-size cover > .avatar display block - float left - margin 0 16px 0 0 - width 58px - height 58px - border-radius 8px - - > .main - float left - width calc(100% - 74px) - - > header - margin-bottom 2px + margin -40px auto 0 auto + width 80px + height 80px + border-radius 100% + border solid 4px $bg - > .name - display inline - margin 0 - padding 0 - color #777 - font-size 1em - font-weight 700 - text-align left - text-decoration none + > .body + padding 4px 32px 32px 32px - &:hover - text-decoration underline + @media (max-width 400px) + padding 4px 16px 16px 16px - > .username - text-align left - margin 0 0 0 8px - color #ccc + > .name + font-size 20px + font-weight bold - > .body - > .followed - display inline-block - margin 0 0 4px 0 - padding 2px 8px - vertical-align top - font-size 10px - color #71afc7 - background #eefaff - border-radius 4px + > .username + display block + opacity 0.7 - > .description - cursor default - display block - margin 0 - padding 0 - overflow-wrap break-word - font-size 1.1em - color #717171 + > .description + margin 16px 0 - > .mk-follow-button - position absolute - top 16px - right 16px + > .followed + margin 0 0 16px 0 + padding 0 + line-height 24px + font-size 0.8em + color #71afc7 + background #eefaff + border-radius 4px </style> diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue index 0423db8ed7..05e2f4e5b3 100644 --- a/src/client/app/desktop/views/components/users-list.vue +++ b/src/client/app/desktop/views/components/users-list.vue @@ -33,7 +33,7 @@ export default Vue.extend({ props: ['fetch', 'count', 'youKnowCount'], data() { return { - limit: 30, + limit: 20, mode: 'all', fetching: true, moreFetching: false, @@ -73,10 +73,14 @@ export default Vue.extend({ .mk-users-list height 100% - background #fff + overflow auto + background #eee > nav - z-index 1 + z-index 10 + position sticky + top 0 + background #fff box-shadow 0 1px 0 rgba(#000, 0.1) > div @@ -114,16 +118,14 @@ export default Vue.extend({ background #eee border-radius 20px - > .users - height calc(100% - 54px) - overflow auto - - > * - border-bottom solid 1px rgba(#000, 0.05) + > button + display block + width calc(100% - 32px) + margin 16px + padding 16px - > * - max-width 600px - margin 0 auto + &:hover + background rgba(#000, 0.1) > .no margin 0 diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue index ec044ad27e..30f0ec558f 100644 --- a/src/client/app/desktop/views/components/window.vue +++ b/src/client/app/desktop/views/components/window.vue @@ -106,7 +106,7 @@ export default Vue.extend({ mounted() { if (this.preventMount) { - this.$destroy(); + this.destroyDom(); return; } @@ -190,7 +190,7 @@ export default Vue.extend({ }); setTimeout(() => { - this.$destroy(); + this.destroyDom(); this.$emit('closed'); }, 300); }, diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue index ebb54d782e..c86c30db17 100644 --- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue +++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue @@ -1,22 +1,34 @@ <template> <div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card"> <header>%i18n:@dashboard%</header> + <div v-if="stats" class="stats"> <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> <div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> <div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> <div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> </div> + <div class="cpu-memory"> <x-cpu-memory :connection="connection"/> </div> - <div> - <label> - <input type="checkbox" v-model="disableRegistration" @change="updateMeta"> - <span>disableRegistration</span> - </label> - <button class="ui" @click="invite">%i18n:@invite%</button> - <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> + + <div class="form"> + <div> + <label> + <input type="checkbox" v-model="disableRegistration" @change="updateMeta"> + <span>%i18n:@disableRegistration%</span> + </label> + <button class="ui" @click="invite">%i18n:@invite%</button> + <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> + </div> + + <div> + <label> + <input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta"> + <span>%i18n:@disableLocalTimeline%</span> + </label> + </div> </div> </div> </template> @@ -33,6 +45,7 @@ export default Vue.extend({ return { stats: null, disableRegistration: false, + disableLocalTimeline: false, inviteCode: null, connection: null, connectionId: null @@ -44,6 +57,7 @@ export default Vue.extend({ (this as any).os.getMeta().then(meta => { this.disableRegistration = meta.disableRegistration; + this.disableLocalTimeline = meta.disableLocalTimeline; }); (this as any).api('stats').then(stats => { @@ -61,7 +75,8 @@ export default Vue.extend({ }, updateMeta() { (this as any).api('admin/update-meta', { - disableRegistration: this.disableRegistration + disableRegistration: this.disableRegistration, + disableLocalTimeline: this.disableLocalTimeline }); } } @@ -97,4 +112,8 @@ export default Vue.extend({ border solid 1px #eee border-radius: 8px + > .form + > div + border-bottom solid 1px #eee + </style> diff --git a/src/client/app/desktop/views/pages/admin/admin.hashtags.vue b/src/client/app/desktop/views/pages/admin/admin.hashtags.vue new file mode 100644 index 0000000000..c6bf20361f --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.hashtags.vue @@ -0,0 +1,41 @@ +<template> +<div class="jdnqwkzlnxcfftthoybjxrebyolvoucw mk-admin-card"> + <header>%i18n:@hided-tags%</header> + <textarea v-model="hidedTags"></textarea> + <button class="ui" @click="save">%i18n:@save%</button> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; + +export default Vue.extend({ + data() { + return { + hidedTags: '', + }; + }, + created() { + (this as any).os.getMeta().then(meta => { + this.hidedTags = meta.hidedTags.join('\n'); + }); + }, + methods: { + save() { + (this as any).api('admin/update-meta', { + hidedTags: this.hidedTags.split('\n') + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.jdnqwkzlnxcfftthoybjxrebyolvoucw + textarea + width 100% + min-height 300px + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue index a71059c378..510252b447 100644 --- a/src/client/app/desktop/views/pages/admin/admin.vue +++ b/src/client/app/desktop/views/pages/admin/admin.vue @@ -5,6 +5,8 @@ <li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li> <li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li> <li @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li> + <li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li> + <!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> --> <!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> --> </ul> @@ -17,6 +19,9 @@ <div v-show="page == 'announcements'"> <x-announcements/> </div> + <div v-show="page == 'hashtags'"> + <x-hashtags/> + </div> <div v-if="page == 'users'"> <x-suspend-user/> <x-unsuspend-user/> @@ -33,6 +38,7 @@ import Vue from "vue"; import XDashboard from "./admin.dashboard.vue"; import XAnnouncements from "./admin.announcements.vue"; +import XHashtags from "./admin.hashtags.vue"; import XSuspendUser from "./admin.suspend-user.vue"; import XUnsuspendUser from "./admin.unsuspend-user.vue"; import XVerifyUser from "./admin.verify-user.vue"; @@ -43,6 +49,7 @@ export default Vue.extend({ components: { XDashboard, XAnnouncements, + XHashtags, XSuspendUser, XUnsuspendUser, XVerifyUser, diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue index 239b1b0447..abb09775fb 100644 --- a/src/client/app/desktop/views/pages/deck/deck.column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.column.vue @@ -28,6 +28,7 @@ import Vue from 'vue'; import Menu from '../../../../common/views/components/menu.vue'; import contextmenu from '../../../api/contextmenu'; +import { countIf } from '../../../../../../prelude/array'; export default Vue.extend({ props: { @@ -117,7 +118,7 @@ export default Vue.extend({ toggleActive() { if (!this.isStacked) return; const vms = this.$store.state.settings.deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id)); - if (this.active && vms.filter(vm => vm.$el.classList.contains('active')).length == 1) return; + if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return; this.active = !this.active; }, diff --git a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue index 70048f99e3..e82e76e4d0 100644 --- a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue +++ b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue @@ -68,7 +68,7 @@ export default Vue.extend({ (this as any).api('notes/user-list-timeline', { listId: this.list.id, limit: fetchLimit + 1, - mediaOnly: this.mediaOnly, + withFiles: this.mediaOnly, includeMyRenotes: this.$store.state.settings.showMyRenotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeLocalRenotes: this.$store.state.settings.showLocalRenotes @@ -90,7 +90,7 @@ export default Vue.extend({ listId: this.list.id, limit: fetchLimit + 1, untilId: (this.$refs.timeline as any).tail().id, - mediaOnly: this.mediaOnly, + withFiles: this.mediaOnly, includeMyRenotes: this.$store.state.settings.showMyRenotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeLocalRenotes: this.$store.state.settings.showLocalRenotes @@ -109,7 +109,7 @@ export default Vue.extend({ return promise; }, onNote(note) { - if (this.mediaOnly && note.media.length == 0) return; + if (this.mediaOnly && note.files.length == 0) return; // Prepend a note (this.$refs.timeline as any).prepend(note); diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue index 2615c0d090..980fb03136 100644 --- a/src/client/app/desktop/views/pages/deck/deck.note.vue +++ b/src/client/app/desktop/views/pages/deck/deck.note.vue @@ -18,7 +18,7 @@ <div class="body"> <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 ? '%i18n:@less%' : '%i18n:@more%' }}</span> + <mk-cw-button v-model="showContent"/> </p> <div class="content" v-show="p.cw == null || showContent"> <div class="text"> @@ -28,8 +28,8 @@ <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/> <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 class="files" v-if="p.files.length > 0"> + <mk-media-list :media-list="p.files"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> @@ -54,11 +54,11 @@ </article> </div> <div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi"> - <div v-if="note.media.length > 0"> - <mk-media-list :media-list="note.media"/> + <div v-if="note.files.length > 0"> + <mk-media-list :media-list="note.files"/> </div> - <div v-if="note.renote && note.renote.media.length > 0"> - <mk-media-list :media-list="note.renote.media"/> + <div v-if="note.renote && note.renote.files.length > 0"> + <mk-media-list :media-list="note.renote.files"/> </div> </div> </template> @@ -100,7 +100,7 @@ export default Vue.extend({ isRenote(): boolean { return (this.note.renote && this.note.text == null && - this.note.mediaIds.length == 0 && + this.note.fileIds.length == 0 && this.note.poll == null); }, @@ -371,7 +371,7 @@ root(isDark) .mk-url-preview margin-top 8px - > .media + > .files > img display block max-width 100% @@ -394,7 +394,7 @@ root(isDark) > .renote margin 8px 0 - > .mk-note-preview + > * padding 16px border dashed 1px isDark ? #4e945e : #c0dac6 border-radius 8px diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue index f7fca5de92..2e7e30f12a 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notes.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue @@ -127,7 +127,7 @@ export default Vue.extend({ prepend(note, silent = false) { //#region 弾く const isMyNote = note.userId == this.$store.state.i.id; - const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; if (this.$store.state.settings.showMyRenotes === false) { if (isMyNote && isPureRenote) { diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue index a9e4d489c3..120ceb7fc2 100644 --- a/src/client/app/desktop/views/pages/deck/deck.tl.vue +++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue @@ -96,7 +96,7 @@ export default Vue.extend({ (this.$refs.timeline as any).init(() => new Promise((res, rej) => { (this as any).api(this.endpoint, { limit: fetchLimit + 1, - mediaOnly: this.mediaOnly, + withFiles: this.mediaOnly, includeMyRenotes: this.$store.state.settings.showMyRenotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeLocalRenotes: this.$store.state.settings.showLocalRenotes @@ -117,7 +117,7 @@ export default Vue.extend({ const promise = (this as any).api(this.endpoint, { limit: fetchLimit + 1, - mediaOnly: this.mediaOnly, + withFiles: this.mediaOnly, untilId: (this.$refs.timeline as any).tail().id, includeMyRenotes: this.$store.state.settings.showMyRenotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, @@ -138,7 +138,7 @@ export default Vue.extend({ }, onNote(note) { - if (this.mediaOnly && note.media.length == 0) return; + if (this.mediaOnly && note.files.length == 0) return; // Prepend a note (this.$refs.timeline as any).prepend(note); diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index 26b989656e..5e7a07ea6b 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -85,6 +85,7 @@ export default Vue.extend({ }, mounted() { + document.title = (this as any).os.instanceName; document.documentElement.style.overflow = 'hidden'; }, diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue index d8f4656ed0..4b434ec219 100644 --- a/src/client/app/desktop/views/pages/user/user.header.vue +++ b/src/client/app/desktop/views/pages/user/user.header.vue @@ -6,7 +6,7 @@ <div class="title"> <p class="name">{{ user | userName }}</p> <div> - <span class="username"><mk-acct :user="user"/></span> + <span class="username"><mk-acct :user="user" :detail="true" /></span> <span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span> <span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span> <span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span> diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index 64c537f1ed..c5cd9e24fe 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -24,12 +24,12 @@ export default Vue.extend({ mounted() { (this as any).api('users/notes', { userId: this.user.id, - withMedia: true, + withFiles: true, limit: 9 }).then(notes => { notes.forEach(note => { - note.media.forEach(media => { - if (this.images.length < 9) this.images.push(media); + note.files.forEach(file => { + if (this.images.length < 9) this.images.push(file); }); }); this.fetching = false; diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue index 67987fcb94..54221380a7 100644 --- a/src/client/app/desktop/views/pages/user/user.timeline.vue +++ b/src/client/app/desktop/views/pages/user/user.timeline.vue @@ -66,7 +66,7 @@ export default Vue.extend({ limit: fetchLimit + 1, untilDate: this.date ? this.date.getTime() : undefined, includeReplies: this.mode == 'with-replies', - withMedia: this.mode == 'with-media' + withFiles: this.mode == 'with-media' }).then(notes => { if (notes.length == fetchLimit + 1) { notes.pop(); @@ -86,7 +86,7 @@ export default Vue.extend({ userId: this.user.id, limit: fetchLimit + 1, includeReplies: this.mode == 'with-replies', - withMedia: this.mode == 'with-media', + withFiles: this.mode == 'with-media', untilId: (this.$refs.timeline as any).tail().id }); diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index 0bc5c256e0..ea1734f8c7 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -7,45 +7,130 @@ <mk-forkit class="forkit"/> - <div class="body"> - <div class="main block"> - <h1 v-if="name != 'Misskey'">{{ name }}</h1> - <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1> + <main> + <div class="body"> + <div class="main block"> + <div> + <h1 v-if="name != 'Misskey'">{{ name }}</h1> + <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1> - <div class="info"> - <span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span> - <span class="stats" v-if="stats"> - <span>%fa:user% {{ stats.originalUsersCount | number }}</span> - <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> - </span> + <div class="info"> + <span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span> + <span class="stats" v-if="stats"> + <span>%fa:user% {{ stats.originalUsersCount | number }}</span> + <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> + </span> + </div> + + <div class="desc"> + <span class="desc" v-html="description || '%i18n:common.about%'"></span> + <a class="about" @click="about">%i18n:@about%</a> + </div> + + <p class="sign"> + <span class="signup" @click="signup">%i18n:@signup%</span> + <span class="divider">|</span> + <span class="signin" @click="signin">%i18n:@signin%</span> + </p> + + <img src="/assets/ai.png" alt="" title="藍" class="char"> + </div> </div> - <p class="desc" v-html="description || '%i18n:common.about%'"></p> + <div class="announcements block"> + <header>%fa:broadcast-tower% %i18n:@announcements%</header> + <div v-if="announcements && announcements.length > 0"> + <div v-for="announcement in announcements"> + <h1 v-html="announcement.title"></h1> + <div v-html="announcement.text"></div> + </div> + </div> + </div> - <p class="sign"> - <span class="signup" @click="signup">%i18n:@signup%</span> - <span class="divider">|</span> - <span class="signin" @click="signin">%i18n:@signin%</span> - </p> - </div> + <div class="photos block"> + <header>%fa:images% %i18n:@photos%</header> + <div> + <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div> + </div> + </div> - <div class="broadcasts block"> - <div v-for="broadcast in broadcasts"> - <h1 v-html="broadcast.title"></h1> - <div v-html="broadcast.text"></div> + <div class="tag-cloud block"> + <div> + <mk-tag-cloud/> + </div> </div> - </div> - <div class="nav block"> - <mk-nav class="nav"/> - </div> + <div class="nav block"> + <div> + <mk-nav class="nav"/> + </div> + </div> + + <div class="side"> + <div class="trends block"> + <div> + <mk-trends/> + </div> + </div> - <div class="side"> - <mk-trends class="trends block"/> + <div class="tl block"> + <header>%fa:comment-alt R% %i18n:@timeline%</header> + <div> + <mk-welcome-timeline class="tl" :max="20"/> + </div> + </div> - <mk-welcome-timeline class="tl block" :max="20"/> + <div class="info block"> + <header>%fa:info-circle% %i18n:@info%</header> + <div> + <div v-if="meta" class="body"> + <p>Version: <b>{{ meta.version }}</b></p> + <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p> + </div> + </div> + </div> + </div> </div> - </div> + </main> + + <modal name="about" :class="$store.state.device.darkmode ? ['about', 'modal-dark'] : ['about', 'modal-light']" width="800px" height="auto" scrollable> + <article class="fpdezooorhntlzyeszemrsqdlgbysvxq"> + <h1>%i18n:common.intro.title%</h1> + <p v-html="'%i18n:common.intro.about%'"></p> + <section> + <h2>%i18n:common.intro.features%</h2> + <section> + <div class="body"> + <h3>%i18n:common.intro.rich-contents%</h3> + <p v-html="'%i18n:common.intro.rich-contents-desc%'"></p> + </div> + <div class="image"><img src="/assets/about/post.png" alt=""></div> + </section> + <section> + <div class="body"> + <h3>%i18n:common.intro.reaction%</h3> + <p v-html="'%i18n:common.intro.reaction-desc%'"></p> + </div> + <div class="image"><img src="/assets/about/reaction.png" alt=""></div> + </section> + <section> + <div class="body"> + <h3>%i18n:common.intro.ui%</h3> + <p v-html="'%i18n:common.intro.ui-desc%'"></p> + </div> + <div class="image"><img src="/assets/about/ui.png" alt=""></div> + </section> + <section> + <div class="body"> + <h3>%i18n:common.intro.drive%</h3> + <p v-html="'%i18n:common.intro.drive-desc%'"></p> + </div> + <div class="image"><img src="/assets/about/drive.png" alt=""></div> + </section> + </section> + <p v-html="'%i18n:common.intro.outro%'"></p> + </article> + </modal> <modal name="signup" :class="$store.state.device.darkmode ? 'modal-dark' : 'modal-light'" width="450px" height="auto" scrollable> <header class="formHeader">%i18n:@signup%</header> @@ -62,37 +147,62 @@ <script lang="ts"> import Vue from 'vue'; import { host, copyright } from '../../../config'; +import { concat } from '../../../../../prelude/array'; export default Vue.extend({ data() { return { + meta: null, stats: null, copyright, host, name: 'Misskey', description: '', - broadcasts: [] + announcements: [], + photos: [] }; }, + created() { (this as any).os.getMeta().then(meta => { + this.meta = meta; this.name = meta.name; this.description = meta.description; - this.broadcasts = meta.broadcasts; + this.announcements = meta.broadcasts; }); (this as any).api('stats').then(stats => { this.stats = stats; }); + const image = [ + 'image/jpeg', + 'image/png', + 'image/gif' + ]; + + (this as any).api('notes/local-timeline', { + fileType: image, + limit: 6 + }).then((notes: any[]) => { + const files = concat(notes.map((n: any): any[] => n.files)); + this.photos = files.filter(f => image.includes(f.type)).slice(0, 6); + }); }, + methods: { + about() { + this.$modal.show('about'); + }, + signup() { this.$modal.show('signup'); }, + signin() { this.$modal.show('signin'); }, + dark() { this.$store.commit('device/set', { key: 'darkmode', @@ -137,6 +247,54 @@ export default Vue.extend({ margin 0 48px font-size 1.5em +.v--modal-overlay.about + .v--modal-box.v--modal + margin 32px 0 + +.fpdezooorhntlzyeszemrsqdlgbysvxq + padding 64px + + > p:last-child + margin-bottom 0 + + > h1 + margin-top 0 + + > section + > h2 + border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) + + > section + display grid + grid-template-rows 1fr + grid-template-columns 180px 1fr + gap 32px + margin-bottom 32px + padding-bottom 32px + border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) + + &:nth-child(odd) + grid-template-columns 1fr 180px + + > .body + grid-column 1 + + > .image + grid-column 2 + + > .body + grid-row 1 + grid-column 2 + + > .image + grid-row 1 + grid-column 1 + + > img + display block + width 100% + height 100% + object-fit cover </style> <style lang="stylus" scoped> @@ -164,116 +322,176 @@ root(isDark) font-size 18px color isDark ? #fff : #444 - > .body - display grid - grid-template-rows 0.5fr 0.5fr 64px - grid-template-columns 1fr 350px - gap 16px - width 100% - max-width 1200px - height 100vh - min-height 800px + > main margin 0 auto padding 64px + width 100% + max-width 1200px .block color isDark ? #fff : #444 - background isDark ? #313543 : #fff + background isDark ? #282C37 : #fff box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) //border-radius 8px overflow auto - > .main - grid-row 1 - grid-column 1 - padding 32px - border-top solid 5px $theme-color + > header + z-index 1 + padding 0 16px + line-height 48px + background isDark ? #313543 : #fff - > h1 - margin 0 + if !isDark + box-shadow 0 1px 0px rgba(0, 0, 0, 0.1) - > img - margin -8px 0 0 -16px - max-width 280px + & + div + max-height calc(100% - 48px) - > .info - margin 0 auto 16px auto - width $width - font-size 14px + > div + overflow auto - > .stats - margin-left 16px - padding-left 16px - border-left solid 1px isDark ? #fff : #444 + > .body + display grid + grid-template-rows 390px 1fr 256px 64px + grid-template-columns 1fr 1fr 350px + gap 16px + height 1150px - > * - margin-right 16px + > .main + grid-row 1 + grid-column 1 / 3 + border-top solid 5px $theme-color - > .sign - font-size 120% + > div + padding 32px + min-height 100% - > .divider - margin 0 16px + > h1 + margin 0 - > .signin - > .signup - cursor pointer + > img + margin -8px 0 0 -16px + max-width 280px - &:hover - color $theme-color + > .info + margin 0 auto 16px auto + width $width + font-size 14px - > .hashtags - margin 16px auto - width $width - font-size 14px - background rgba(#000, 0.3) - border-radius 8px + > .stats + margin-left 16px + padding-left 16px + border-left solid 1px isDark ? #fff : #444 - > * - display inline-block - margin 14px + > * + margin-right 16px - > .broadcasts - grid-row 2 - grid-column 1 - padding 32px + > .desc + max-width calc(100% - 150px) - > div - padding 0 0 16px 0 - margin 0 0 16px 0 - border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) + > .sign + font-size 120% + margin-bottom 0 - > h1 - margin 0 - font-size 1.5em + > .divider + margin 0 16px - > .nav - display flex - justify-content center - align-items center - grid-row 3 - grid-column 1 - font-size 14px + > .signin + > .signup + cursor pointer - > .side - display grid - grid-row 1 / 4 - grid-column 2 - grid-template-rows 1fr 350px - grid-template-columns 1fr - gap 16px + &:hover + color $theme-color - > .tl - grid-row 1 - grid-column 1 - text-align left - max-height 100% - overflow auto + > .char + display block + position absolute + right 16px + bottom 0 + height 320px + opacity 0.7 - > .trends + > *:not(.char) + z-index 1 + + > .announcements grid-row 2 grid-column 1 - padding 8px + + > div + padding 32px + + > div + padding 0 0 16px 0 + margin 0 0 16px 0 + border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) + + > h1 + margin 0 + font-size 1.25em + + > .photos + grid-row 2 + grid-column 2 + + > div + display grid + grid-template-rows 1fr 1fr 1fr + grid-template-columns 1fr 1fr + gap 8px + height 100% + padding 16px + + > div + //border-radius 4px + background-position center center + background-size cover + + > .tag-cloud + grid-row 3 + grid-column 1 / 3 + + > div + height 256px + padding 32px + + > .nav + display flex + justify-content center + align-items center + grid-row 4 + grid-column 1 / 3 + font-size 14px + + > .side + display grid + grid-row 1 / 5 + grid-column 3 + grid-template-rows 1fr 350px + grid-template-columns 1fr + gap 16px + + > .tl + grid-row 1 + grid-column 1 + overflow auto + + > .trends + grid-row 2 + grid-column 1 + padding 8px + + > .info + grid-row 3 + grid-column 1 + + > div + padding 16px + + > .body + > p + display block + margin 0 .mk-welcome[data-darkmode] root(true) diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue index c33bf2f2f2..aeaab63ac4 100644 --- a/src/client/app/desktop/views/widgets/trends.vue +++ b/src/client/app/desktop/views/widgets/trends.vue @@ -49,7 +49,7 @@ export default define({ offset: this.offset, renote: false, reply: false, - media: false, + file: false, poll: false }).then(notes => { const note = notes ? notes[0] : null; |