This commit is contained in:
syuilo 2018-04-22 17:04:52 +09:00
parent 4e032a9188
commit d2d3a7d52b
7 changed files with 374 additions and 232 deletions

View File

@ -34,24 +34,30 @@
<p class="channel" v-if="p.channel"> <p class="channel" v-if="p.channel">
<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>: <a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
</p> </p>
<div class="text"> <p v-if="p.cw != null" class="cw">
<a class="reply" v-if="p.reply">%fa:reply%</a> <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/> <span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span>
<a class="rp" v-if="p.renote">RP:</a> </p>
<div class="content" v-show="p.cw == null || showContent">
<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">RP:</a>
</div>
<div class="media" v-if="p.media.length > 0">
<mk-media-list :media-list="p.media"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
</div>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
</div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div> </div>
<div class="media" v-if="p.media.length > 0">
<mk-media-list :media-list="p.media"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
</div>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
</div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div> </div>
<footer> <footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/> <mk-reactions-viewer :note="p" ref="reactionsViewer"/>
@ -113,6 +119,7 @@ export default Vue.extend({
data() { data() {
return { return {
showContent: false,
isDetailOpened: false, isDetailOpened: false,
connection: null, connection: null,
connectionId: null connectionId: null
@ -456,7 +463,7 @@ root(isDark)
> .body > .body
> .text > .cw
cursor default cursor default
display block display block
margin 0 margin 0
@ -465,90 +472,117 @@ root(isDark)
font-size 1.1em font-size 1.1em
color isDark ? #fff : #717171 color isDark ? #fff : #717171
>>> .title > .text
display block
margin-bottom 4px
padding 4px
font-size 90%
text-align center
background isDark ? #2f3944 : #eef1f3
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 margin-right 8px
color isDark ? #99abbf : #717171
> .rp > .toggle
margin-left 4px
font-style oblique
color #a0bf46
> .location
margin 4px 0
font-size 12px
color #ccc
> .map
width 100%
height 300px
&:empty
display none
> .tags
margin 4px 0 0 0
> *
display inline-block display inline-block
margin 0 8px 0 0 padding 4px 8px
padding 2px 8px 2px 16px font-size 0.7em
font-size 90% color isDark ? #393f4f : #fff
color #8d969e background isDark ? #687390 : #b1b9c1
background #edf0f3 border-radius 2px
border-radius 4px cursor pointer
user-select none
&:before
content ""
display block
position absolute
top 0
bottom 0
left 4px
width 8px
height 8px
margin auto 0
background #fff
border-radius 100%
&:hover &:hover
text-decoration none background isDark ? #707b97 : #bbc4ce
background #e2e7ec
.mk-url-preview > .content
margin-top 8px
> .channel > .text
margin 0 cursor default
display block
margin 0
padding 0
overflow-wrap break-word
font-size 1.1em
color isDark ? #fff : #717171
> .mk-poll >>> .title
font-size 80% display block
margin-bottom 4px
padding 4px
font-size 90%
text-align center
background isDark ? #2f3944 : #eef1f3
border-radius 4px
> .renote >>> .code
margin 8px 0 margin 8px 0
> .mk-note-preview >>> .quote
padding 16px margin 8px
border dashed 1px isDark ? #4e945e : #c0dac6 padding 6px 12px
border-radius 8px 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
> .location
margin 4px 0
font-size 12px
color #ccc
> .map
width 100%
height 300px
&:empty
display none
> .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%
&:hover
text-decoration none
background #e2e7ec
.mk-url-preview
margin-top 8px
> .channel
margin 0
> .mk-poll
font-size 80%
> .renote
margin 8px 0
> .mk-note-preview
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
> footer > footer
> button > button

View File

@ -6,6 +6,7 @@
@drop.stop="onDrop" @drop.stop="onDrop"
> >
<div class="content"> <div class="content">
<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
<textarea :class="{ with: (files.length != 0 || poll) }" <textarea :class="{ with: (files.length != 0 || poll) }"
ref="text" v-model="text" :disabled="posting" ref="text" v-model="text" :disabled="posting"
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
@ -27,6 +28,7 @@
<button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button> <button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button>
<button class="kao" title="%i18n:@insert-a-kao%" @click="kao">%fa:R smile%</button> <button class="kao" title="%i18n:@insert-a-kao%" @click="kao">%fa:R smile%</button>
<button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button> <button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button>
<button class="poll" title="内容を隠す" @click="useCw = !useCw">%fa:eye-slash%</button>
<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> <button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
<p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:!@text-remain%'.replace('{}', 1000 - text.length) }}</p> <p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:!@text-remain%'.replace('{}', 1000 - text.length) }}</p>
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post"> <button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
@ -46,7 +48,9 @@ export default Vue.extend({
components: { components: {
XDraggable XDraggable
}, },
props: ['reply', 'renote'], props: ['reply', 'renote'],
data() { data() {
return { return {
posting: false, posting: false,
@ -54,11 +58,14 @@ export default Vue.extend({
files: [], files: [],
uploadings: [], uploadings: [],
poll: false, poll: false,
useCw: false,
cw: null,
geo: null, geo: null,
autocomplete: null, autocomplete: null,
draghover: false draghover: false
}; };
}, },
computed: { computed: {
draftId(): string { draftId(): string {
return this.renote return this.renote
@ -67,6 +74,7 @@ export default Vue.extend({
? 'reply:' + this.reply.id ? 'reply:' + this.reply.id
: 'note'; : 'note';
}, },
placeholder(): string { placeholder(): string {
return this.renote return this.renote
? '%i18n:!@quote-placeholder%' ? '%i18n:!@quote-placeholder%'
@ -74,6 +82,7 @@ export default Vue.extend({
? '%i18n:!@reply-placeholder%' ? '%i18n:!@reply-placeholder%'
: '%i18n:!@note-placeholder%'; : '%i18n:!@note-placeholder%';
}, },
submitText(): string { submitText(): string {
return this.renote return this.renote
? '%i18n:!@renote%' ? '%i18n:!@renote%'
@ -81,21 +90,26 @@ export default Vue.extend({
? '%i18n:!@reply%' ? '%i18n:!@reply%'
: '%i18n:!@note%'; : '%i18n:!@note%';
}, },
canPost(): boolean { canPost(): boolean {
return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote); return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote);
} }
}, },
watch: { watch: {
text() { text() {
this.saveDraft(); this.saveDraft();
}, },
poll() { poll() {
this.saveDraft(); this.saveDraft();
}, },
files() { files() {
this.saveDraft(); this.saveDraft();
} }
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
// 稿 // 稿
@ -113,13 +127,16 @@ export default Vue.extend({
} }
}); });
}, },
methods: { methods: {
focus() { focus() {
(this.$refs.text as any).focus(); (this.$refs.text as any).focus();
}, },
chooseFile() { chooseFile() {
(this.$refs.file as any).click(); (this.$refs.file as any).click();
}, },
chooseFileFromDrive() { chooseFileFromDrive() {
(this as any).apis.chooseDriveFile({ (this as any).apis.chooseDriveFile({
multiple: true multiple: true
@ -127,32 +144,40 @@ export default Vue.extend({
files.forEach(this.attachMedia); files.forEach(this.attachMedia);
}); });
}, },
attachMedia(driveFile) { attachMedia(driveFile) {
this.files.push(driveFile); this.files.push(driveFile);
this.$emit('change-attached-media', this.files); this.$emit('change-attached-media', this.files);
}, },
detachMedia(id) { detachMedia(id) {
this.files = this.files.filter(x => x.id != id); this.files = this.files.filter(x => x.id != id);
this.$emit('change-attached-media', this.files); this.$emit('change-attached-media', this.files);
}, },
onChangeFile() { onChangeFile() {
Array.from((this.$refs.file as any).files).forEach(this.upload); Array.from((this.$refs.file as any).files).forEach(this.upload);
}, },
upload(file) { upload(file) {
(this.$refs.uploader as any).upload(file); (this.$refs.uploader as any).upload(file);
}, },
onChangeUploadings(uploads) { onChangeUploadings(uploads) {
this.$emit('change-uploadings', uploads); this.$emit('change-uploadings', uploads);
}, },
clear() { clear() {
this.text = ''; this.text = '';
this.files = []; this.files = [];
this.poll = false; this.poll = false;
this.$emit('change-attached-media', this.files); this.$emit('change-attached-media', this.files);
}, },
onKeydown(e) { onKeydown(e) {
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
}, },
onPaste(e) { onPaste(e) {
Array.from(e.clipboardData.items).forEach((item: any) => { Array.from(e.clipboardData.items).forEach((item: any) => {
if (item.kind == 'file') { if (item.kind == 'file') {
@ -160,6 +185,7 @@ export default Vue.extend({
} }
}); });
}, },
onDragover(e) { onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file'; const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
@ -169,12 +195,15 @@ export default Vue.extend({
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
} }
}, },
onDragenter(e) { onDragenter(e) {
this.draghover = true; this.draghover = true;
}, },
onDragleave(e) { onDragleave(e) {
this.draghover = false; this.draghover = false;
}, },
onDrop(e): void { onDrop(e): void {
this.draghover = false; this.draghover = false;
@ -195,6 +224,7 @@ export default Vue.extend({
} }
//#endregion //#endregion
}, },
setGeo() { setGeo() {
if (navigator.geolocation == null) { if (navigator.geolocation == null) {
alert('お使いの端末は位置情報に対応していません'); alert('お使いの端末は位置情報に対応していません');
@ -210,10 +240,12 @@ export default Vue.extend({
enableHighAccuracy: true enableHighAccuracy: true
}); });
}, },
removeGeo() { removeGeo() {
this.geo = null; this.geo = null;
this.$emit('geo-dettached'); this.$emit('geo-dettached');
}, },
post() { post() {
this.posting = true; this.posting = true;
@ -223,6 +255,7 @@ export default Vue.extend({
replyId: this.reply ? this.reply.id : undefined, replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined, renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined, poll: this.poll ? (this.$refs.poll as any).get() : undefined,
cw: this.useCw ? this.cw || '' : undefined,
geo: this.geo ? { geo: this.geo ? {
coordinates: [this.geo.longitude, this.geo.latitude], coordinates: [this.geo.longitude, this.geo.latitude],
altitude: this.geo.altitude, altitude: this.geo.altitude,
@ -250,6 +283,7 @@ export default Vue.extend({
this.posting = false; this.posting = false;
}); });
}, },
saveDraft() { saveDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}'); const data = JSON.parse(localStorage.getItem('drafts') || '{}');
@ -264,6 +298,7 @@ export default Vue.extend({
localStorage.setItem('drafts', JSON.stringify(data)); localStorage.setItem('drafts', JSON.stringify(data));
}, },
deleteDraft() { deleteDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}'); const data = JSON.parse(localStorage.getItem('drafts') || '{}');
@ -271,6 +306,7 @@ export default Vue.extend({
localStorage.setItem('drafts', JSON.stringify(data)); localStorage.setItem('drafts', JSON.stringify(data));
}, },
kao() { kao() {
this.text += getKao(); this.text += getKao();
} }
@ -293,47 +329,54 @@ root(isDark)
> .content > .content
textarea > input
> textarea
display block display block
padding 12px
margin 0
width 100% width 100%
max-width 100% padding 12px
min-width 100%
min-height calc(16px + 12px + 12px)
font-size 16px font-size 16px
color isDark ? #fff : #333 color isDark ? #fff : #333
background isDark ? #191d23 : #fff background isDark ? #191d23 : #fff
outline none outline none
border solid 1px rgba($theme-color, 0.1) border solid 1px rgba($theme-color, 0.1)
border-radius 4px border-radius 4px
transition border-color .3s ease transition border-color .2s ease
&:hover &:hover
border-color rgba($theme-color, 0.2) border-color rgba($theme-color, 0.2)
transition border-color .1s ease transition border-color .1s ease
& + *
& + * + *
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus &:focus
color $theme-color
border-color rgba($theme-color, 0.5) border-color rgba($theme-color, 0.5)
transition border-color 0s ease transition border-color 0s ease
& + *
& + * + *
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
&:disabled &:disabled
opacity 0.5 opacity 0.5
&::-webkit-input-placeholder &::-webkit-input-placeholder
color rgba($theme-color, 0.3) color rgba($theme-color, 0.3)
> input
margin-bottom 8px
> textarea
margin 0
max-width 100%
min-width 100%
min-height 64px
&:hover
& + *
& + * + *
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus
& + *
& + * + *
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
&.with &.with
border-bottom solid 1px rgba($theme-color, 0.1) !important border-bottom solid 1px rgba($theme-color, 0.1) !important
border-radius 4px 4px 0 0 border-radius 4px 4px 0 0

View File

@ -31,27 +31,33 @@
</header> </header>
<div class="body"> <div class="body">
<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p> <p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
<div class="text"> <p v-if="p.cw != null" class="cw">
<a class="reply" v-if="p.reply"> <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
%fa:reply% <span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span>
</a> </p>
<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/> <div class="content" v-show="p.cw == null || showContent">
<a class="rp" v-if="p.renote != null">RP:</a> <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>
</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>
<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> <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> </div>
<footer> <footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/> <mk-reactions-viewer :note="p" ref="reactionsViewer"/>
@ -92,6 +98,7 @@ export default Vue.extend({
data() { data() {
return { return {
showContent: false,
connection: null, connection: null,
connectionId: null connectionId: null
}; };
@ -229,7 +236,7 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '~const.styl' @import '~const.styl'
.note root(isDark)
font-size 12px font-size 12px
border-bottom solid 1px #eaeaea border-bottom solid 1px #eaeaea
@ -388,113 +395,140 @@ export default Vue.extend({
> .body > .body
> .text > .cw
cursor default
display block display block
margin 0 margin 0
padding 0 padding 0
overflow-wrap break-word overflow-wrap break-word
font-size 1.1em font-size 1.1em
color #717171 color isDark ? #fff : #717171
>>> .title > .text
display block
margin-bottom 4px
padding 4px
font-size 90%
text-align center
background #eef1f3
border-radius 4px
>>> .code
margin 8px 0
>>> .quote
margin 8px
padding 6px 12px
color #aaa
border-left solid 3px #eee
> .reply
margin-right 8px 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
display block
margin 0
padding 0
overflow-wrap break-word
font-size 1.1em
color #717171 color #717171
> .rp >>> .title
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 display block
position absolute margin-bottom 4px
top 0 padding 4px
bottom 0 font-size 90%
left 4px text-align center
width 8px background #eef1f3
height 8px border-radius 4px
margin auto 0
background #fff
border-radius 100%
> .media >>> .code
> img margin 8px 0
display block
max-width 100%
> .location >>> .quote
margin 4px 0 margin 8px
font-size 12px padding 6px 12px
color #ccc color #aaa
border-left solid 3px #eee
> .map > .reply
width 100% margin-right 8px
height 200px color #717171
&:empty > .rp
display none 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%
> .media
> img
display block
max-width 100%
> .location
margin 4px 0
font-size 12px
color #ccc
> .map
width 100%
height 200px
&:empty
display none
> .mk-poll
font-size 80%
> .renote
margin 8px 0
> .mk-note-preview
padding 16px
border dashed 1px #c0dac6
border-radius 8px
> .app > .app
font-size 12px font-size 12px
color #ccc color #ccc
> .mk-poll
font-size 80%
> .renote
margin 8px 0
> .mk-note-preview
padding 16px
border dashed 1px #c0dac6
border-radius 8px
> footer > footer
> button > button
margin 0 margin 0
@ -524,6 +558,12 @@ export default Vue.extend({
@media (max-width 350px) @media (max-width 350px)
display none display none
.note[data-darkmode]
root(true)
.note:not([data-darkmode])
root(false)
</style> </style>
<style lang="stylus" module> <style lang="stylus" module>

View File

@ -10,6 +10,7 @@
</header> </header>
<div class="form"> <div class="form">
<mk-note-preview v-if="reply" :note="reply"/> <mk-note-preview v-if="reply" :note="reply"/>
<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> <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"> <div class="attaches" v-show="files.length != 0">
<x-draggable class="files" :list="files" :options="{ animation: 150 }"> <x-draggable class="files" :list="files" :options="{ animation: 150 }">
@ -24,6 +25,7 @@
<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button> <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
<button class="kao" @click="kao">%fa:R smile%</button> <button class="kao" @click="kao">%fa:R smile%</button>
<button class="poll" @click="poll = true">%fa:chart-pie%</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="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/> <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
</div> </div>
@ -39,7 +41,9 @@ export default Vue.extend({
components: { components: {
XDraggable XDraggable
}, },
props: ['reply'], props: ['reply'],
data() { data() {
return { return {
posting: false, posting: false,
@ -47,21 +51,27 @@ export default Vue.extend({
uploadings: [], uploadings: [],
files: [], files: [],
poll: false, poll: false,
geo: null geo: null,
useCw: false,
cw: null
}; };
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
this.focus(); this.focus();
}); });
}, },
methods: { methods: {
focus() { focus() {
(this.$refs.text as any).focus(); (this.$refs.text as any).focus();
}, },
chooseFile() { chooseFile() {
(this.$refs.file as any).click(); (this.$refs.file as any).click();
}, },
chooseFileFromDrive() { chooseFileFromDrive() {
(this as any).apis.chooseDriveFile({ (this as any).apis.chooseDriveFile({
multiple: true multiple: true
@ -69,23 +79,29 @@ export default Vue.extend({
files.forEach(this.attachMedia); files.forEach(this.attachMedia);
}); });
}, },
attachMedia(driveFile) { attachMedia(driveFile) {
this.files.push(driveFile); this.files.push(driveFile);
this.$emit('change-attached-media', this.files); this.$emit('change-attached-media', this.files);
}, },
detachMedia(file) { detachMedia(file) {
this.files = this.files.filter(x => x.id != file.id); this.files = this.files.filter(x => x.id != file.id);
this.$emit('change-attached-media', this.files); this.$emit('change-attached-media', this.files);
}, },
onChangeFile() { onChangeFile() {
Array.from((this.$refs.file as any).files).forEach(this.upload); Array.from((this.$refs.file as any).files).forEach(this.upload);
}, },
upload(file) { upload(file) {
(this.$refs.uploader as any).upload(file); (this.$refs.uploader as any).upload(file);
}, },
onChangeUploadings(uploads) { onChangeUploadings(uploads) {
this.$emit('change-uploadings', uploads); this.$emit('change-uploadings', uploads);
}, },
setGeo() { setGeo() {
if (navigator.geolocation == null) { if (navigator.geolocation == null) {
alert('お使いの端末は位置情報に対応していません'); alert('お使いの端末は位置情報に対応していません');
@ -100,15 +116,18 @@ export default Vue.extend({
enableHighAccuracy: true enableHighAccuracy: true
}); });
}, },
removeGeo() { removeGeo() {
this.geo = null; this.geo = null;
}, },
clear() { clear() {
this.text = ''; this.text = '';
this.files = []; this.files = [];
this.poll = false; this.poll = false;
this.$emit('change-attached-media'); this.$emit('change-attached-media');
}, },
post() { post() {
this.posting = true; this.posting = true;
const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true; const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true;
@ -117,6 +136,7 @@ export default Vue.extend({
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined, replyId: this.reply ? this.reply.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined, poll: this.poll ? (this.$refs.poll as any).get() : undefined,
cw: this.useCw ? this.cw || '' : undefined,
geo: this.geo ? { geo: this.geo ? {
coordinates: [this.geo.longitude, this.geo.latitude], coordinates: [this.geo.longitude, this.geo.latitude],
altitude: this.geo.altitude, altitude: this.geo.altitude,
@ -133,10 +153,12 @@ export default Vue.extend({
this.posting = false; this.posting = false;
}); });
}, },
cancel() { cancel() {
this.$emit('cancel'); this.$emit('cancel');
this.$destroy(); this.$destroy();
}, },
kao() { kao() {
this.text += getKao(); this.text += getKao();
} }
@ -236,14 +258,12 @@ export default Vue.extend({
> .file > .file
display none display none
> input
> textarea > textarea
display block display block
padding 12px padding 12px
margin 0 margin 0
width 100% width 100%
max-width 100%
min-width 100%
min-height 80px
font-size 16px font-size 16px
color #333 color #333
border none border none
@ -253,6 +273,11 @@ export default Vue.extend({
&:disabled &:disabled
opacity 0.5 opacity 0.5
> textarea
max-width 100%
min-width 100%
min-height 80px
> .upload > .upload
> .drive > .drive
> .kao > .kao

View File

@ -24,7 +24,7 @@ export function isValidText(text: string): boolean {
} }
export function isValidCw(text: string): boolean { export function isValidCw(text: string): boolean {
return text.length <= 100 && text.trim() != ''; return text.length <= 100;
} }
export type INote = { export type INote = {

View File

@ -23,11 +23,11 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
if (visibilityErr) return rej('invalid visibility'); if (visibilityErr) return rej('invalid visibility');
// Get 'text' parameter // Get 'text' parameter
const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; const [text = null, textErr] = $(params.text).optional.nullable.string().pipe(isValidText).$;
if (textErr) return rej('invalid text'); if (textErr) return rej('invalid text');
// Get 'cw' parameter // Get 'cw' parameter
const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$; const [cw, cwErr] = $(params.cw).optional.nullable.string().pipe(isValidCw).$;
if (cwErr) return rej('invalid cw'); if (cwErr) return rej('invalid cw');
// Get 'viaMobile' parameter // Get 'viaMobile' parameter
@ -187,14 +187,14 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
const note = await create(user, { const note = await create(user, {
createdAt: new Date(), createdAt: new Date(),
media: files, media: files,
poll: poll, poll,
text: text, text,
reply, reply,
renote, renote,
cw: cw, cw,
tags: tags, tags,
app: app, app,
viaMobile: viaMobile, viaMobile,
visibility, visibility,
geo geo
}); });

View File

@ -63,7 +63,7 @@ export default async (user: IUser, data: {
renoteId: data.renote ? data.renote._id : null, renoteId: data.renote ? data.renote._id : null,
text: data.text, text: data.text,
poll: data.poll, poll: data.poll,
cw: data.cw, cw: data.cw == null ? null : data.cw,
tags, tags,
userId: user._id, userId: user._id,
viaMobile: data.viaMobile, viaMobile: data.viaMobile,