wip
This commit is contained in:
parent
b9cb703f97
commit
ff67563dc8
16 changed files with 57 additions and 97 deletions
126
src/web/app/mobile/views/components/friends-maker.vue
Normal file
126
src/web/app/mobile/views/components/friends-maker.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="mk-friends-maker">
|
||||
<p class="title">気になるユーザーをフォロー:</p>
|
||||
<div class="users" v-if="!fetching && users.length > 0">
|
||||
<template each={ users }>
|
||||
<mk-user-card user={ this } />
|
||||
</template>
|
||||
</div>
|
||||
<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
|
||||
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
|
||||
<a class="refresh" @click="refresh">もっと見る</a>
|
||||
<button class="close" @click="close" title="閉じる">%fa:times%</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
users: [],
|
||||
fetching: true,
|
||||
limit: 6,
|
||||
page: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
this.fetching = true;
|
||||
this.users = [];
|
||||
|
||||
this.$root.$data.os.api('users/recommendation', {
|
||||
limit: this.limit,
|
||||
offset: this.limit * this.page
|
||||
}).then(users => {
|
||||
this.fetching = false;
|
||||
this.users = users;
|
||||
});
|
||||
},
|
||||
refresh() {
|
||||
if (this.users.length < this.limit) {
|
||||
this.page = 0;
|
||||
} else {
|
||||
this.page++;
|
||||
}
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-friends-maker
|
||||
background #fff
|
||||
border-radius 8px
|
||||
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> .title
|
||||
margin 0
|
||||
padding 8px 16px
|
||||
font-size 1em
|
||||
font-weight bold
|
||||
color #888
|
||||
|
||||
> .users
|
||||
overflow-x scroll
|
||||
-webkit-overflow-scrolling touch
|
||||
white-space nowrap
|
||||
padding 16px
|
||||
background #eee
|
||||
|
||||
> mk-user-card
|
||||
&:not(:last-child)
|
||||
margin-right 16px
|
||||
|
||||
> .empty
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .refresh
|
||||
display block
|
||||
margin 0
|
||||
padding 8px 16px
|
||||
text-align right
|
||||
font-size 0.9em
|
||||
color #999
|
||||
|
||||
> .close
|
||||
cursor pointer
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1.2em
|
||||
color #999
|
||||
border none
|
||||
outline none
|
||||
background transparent
|
||||
|
||||
&:hover
|
||||
color #555
|
||||
|
||||
&:active
|
||||
color #222
|
||||
|
||||
> [data-fa]
|
||||
padding 10px
|
||||
|
||||
</style>
|
||||
37
src/web/app/mobile/views/components/images-image.vue
Normal file
37
src/web/app/mobile/views/components/images-image.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div>
|
||||
<a class="mk-images-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['image'],
|
||||
computed: {
|
||||
style(): any {
|
||||
return {
|
||||
'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
|
||||
'background-image': `url(${this.image.url}?thumbnail&size=512)`
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-images-image
|
||||
display block
|
||||
overflow hidden
|
||||
border-radius 4px
|
||||
|
||||
> a
|
||||
display block
|
||||
overflow hidden
|
||||
width 100%
|
||||
height 100%
|
||||
background-position center
|
||||
background-size cover
|
||||
|
||||
</style>
|
||||
204
src/web/app/mobile/views/components/post-form.vue
Normal file
204
src/web/app/mobile/views/components/post-form.vue
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
<template>
|
||||
<div class="mk-post-form">
|
||||
<header>
|
||||
<button class="cancel" @click="cancel">%fa:times%</button>
|
||||
<div>
|
||||
<span v-if="refs.text" class="text-count { over: refs.text.value.length > 1000 }">{ 1000 - refs.text.value.length }</span>
|
||||
<button class="submit" @click="post">%i18n:mobile.tags.mk-post-form.submit%</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="form">
|
||||
<mk-post-preview v-if="opts.reply" post={ opts.reply }/>
|
||||
<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea>
|
||||
<div class="attaches" show={ files.length != 0 }>
|
||||
<ul class="files" ref="attaches">
|
||||
<li class="file" each={ files } data-id={ id }>
|
||||
<div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" @click="removeFile"></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
|
||||
<mk-uploader @uploaded="attachMedia" @change="onChangeUploadings"/>
|
||||
<button ref="upload" @click="selectFile">%fa:upload%</button>
|
||||
<button ref="drive" @click="selectFileFromDrive">%fa:cloud%</button>
|
||||
<button class="kao" @click="kao">%fa:R smile%</button>
|
||||
<button class="poll" @click="addPoll">%fa:chart-pie%</button>
|
||||
<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
|
||||
</div>
|
||||
</div
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Sortable from 'sortablejs';
|
||||
import getKao from '../../common/scripts/get-kao';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
posting: false,
|
||||
text: '',
|
||||
uploadings: [],
|
||||
files: [],
|
||||
poll: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
(this.$refs.text as any).focus();
|
||||
|
||||
new Sortable(this.$refs.attaches, {
|
||||
animation: 150
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
attachMedia(driveFile) {
|
||||
this.files.push(driveFile);
|
||||
this.$emit('change-attached-media', this.files);
|
||||
},
|
||||
detachMedia(id) {
|
||||
this.files = this.files.filter(x => x.id != id);
|
||||
this.$emit('change-attached-media', this.files);
|
||||
},
|
||||
onChangeFile() {
|
||||
Array.from((this.$refs.file as any).files).forEach(this.upload);
|
||||
},
|
||||
upload(file) {
|
||||
(this.$refs.uploader as any).upload(file);
|
||||
},
|
||||
onChangeUploadings(uploads) {
|
||||
this.$emit('change-uploadings', uploads);
|
||||
},
|
||||
clear() {
|
||||
this.text = '';
|
||||
this.files = [];
|
||||
this.poll = false;
|
||||
this.$emit('change-attached-media');
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('cancel');
|
||||
this.$destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-post-form
|
||||
max-width 500px
|
||||
width calc(100% - 16px)
|
||||
margin 8px auto
|
||||
background #fff
|
||||
border-radius 8px
|
||||
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
|
||||
|
||||
@media (min-width 500px)
|
||||
margin 16px auto
|
||||
width calc(100% - 32px)
|
||||
|
||||
> header
|
||||
z-index 1
|
||||
height 50px
|
||||
box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1)
|
||||
|
||||
> .cancel
|
||||
width 50px
|
||||
line-height 50px
|
||||
font-size 24px
|
||||
color #555
|
||||
|
||||
> div
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
|
||||
> .text-count
|
||||
line-height 50px
|
||||
color #657786
|
||||
|
||||
> .submit
|
||||
margin 8px
|
||||
padding 0 16px
|
||||
line-height 34px
|
||||
color $theme-color-foreground
|
||||
background $theme-color
|
||||
border-radius 4px
|
||||
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
|
||||
> .form
|
||||
max-width 500px
|
||||
margin 0 auto
|
||||
|
||||
> mk-post-preview
|
||||
padding 16px
|
||||
|
||||
> .attaches
|
||||
|
||||
> .files
|
||||
display block
|
||||
margin 0
|
||||
padding 4px
|
||||
list-style none
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .file
|
||||
display block
|
||||
float left
|
||||
margin 0
|
||||
padding 0
|
||||
border solid 4px transparent
|
||||
|
||||
> .img
|
||||
width 64px
|
||||
height 64px
|
||||
background-size cover
|
||||
background-position center center
|
||||
|
||||
> mk-uploader
|
||||
margin 8px 0 0 0
|
||||
padding 8px
|
||||
|
||||
> [ref='file']
|
||||
display none
|
||||
|
||||
> [ref='text']
|
||||
display block
|
||||
padding 12px
|
||||
margin 0
|
||||
width 100%
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
min-height 80px
|
||||
font-size 16px
|
||||
color #333
|
||||
border none
|
||||
border-bottom solid 1px #ddd
|
||||
border-radius 0
|
||||
|
||||
&:disabled
|
||||
opacity 0.5
|
||||
|
||||
> [ref='upload']
|
||||
> [ref='drive']
|
||||
.kao
|
||||
.poll
|
||||
display inline-block
|
||||
padding 0
|
||||
margin 0
|
||||
width 48px
|
||||
height 48px
|
||||
font-size 20px
|
||||
color #657786
|
||||
background transparent
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
|
||||
</style>
|
||||
|
||||
99
src/web/app/mobile/views/components/post-preview.vue
Normal file
99
src/web/app/mobile/views/components/post-preview.vue
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<div class="mk-post-preview">
|
||||
<a class="avatar-anchor" :href="`/${post.user.username}`">
|
||||
<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
|
||||
</a>
|
||||
<div class="main">
|
||||
<header>
|
||||
<a class="name" :href="`/${post.user.username}`">{{ post.user.name }}</a>
|
||||
<span class="username">@{{ post.user.username }}</span>
|
||||
<a class="time" :href="`/${post.user.username}/${post.id}`">
|
||||
<mk-time :time="post.created_at"/>
|
||||
</a>
|
||||
</header>
|
||||
<div class="body">
|
||||
<mk-sub-post-content class="text" :post="post"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['post']
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-post-preview
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 0.9em
|
||||
background #fff
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 12px 0 0
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 48px
|
||||
height 48px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 60px)
|
||||
|
||||
> header
|
||||
display flex
|
||||
margin-bottom 4px
|
||||
white-space nowrap
|
||||
|
||||
> .name
|
||||
display block
|
||||
margin 0 .5em 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
color #607073
|
||||
font-size 1em
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
text-overflow ellipsis
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 .5em 0 0
|
||||
color #d1d8da
|
||||
|
||||
> .time
|
||||
margin-left auto
|
||||
color #b2b8bb
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1.1em
|
||||
color #717171
|
||||
|
||||
</style>
|
||||
117
src/web/app/mobile/views/components/posts-post-sub.vue
Normal file
117
src/web/app/mobile/views/components/posts-post-sub.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div class="mk-posts-post-sub">
|
||||
<article>
|
||||
<a class="avatar-anchor" href={ '/' + post.user.username }>
|
||||
<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/>
|
||||
</a>
|
||||
<div class="main">
|
||||
<header>
|
||||
<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>
|
||||
<span class="username">@{ post.user.username }</span>
|
||||
<a class="created-at" href={ '/' + post.user.username + '/' + post.id }>
|
||||
<mk-time time={ post.created_at }/>
|
||||
</a>
|
||||
</header>
|
||||
<div class="body">
|
||||
<mk-sub-post-content class="text" post={ post }/>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['post']
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-posts-post-sub
|
||||
font-size 0.9em
|
||||
|
||||
> article
|
||||
padding 16px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
&:hover
|
||||
> .main > footer > button
|
||||
color #888
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 10px 0 0
|
||||
|
||||
@media (min-width 500px)
|
||||
margin-right 16px
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 44px
|
||||
height 44px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
vertical-align bottom
|
||||
|
||||
@media (min-width 500px)
|
||||
width 52px
|
||||
height 52px
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 54px)
|
||||
|
||||
@media (min-width 500px)
|
||||
width calc(100% - 68px)
|
||||
|
||||
> header
|
||||
display flex
|
||||
margin-bottom 2px
|
||||
white-space nowrap
|
||||
|
||||
> .name
|
||||
display block
|
||||
margin 0 0.5em 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
color #607073
|
||||
font-size 1em
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
text-overflow ellipsis
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0
|
||||
color #d1d8da
|
||||
|
||||
> .created-at
|
||||
margin-left auto
|
||||
color #b2b8bb
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1.1em
|
||||
color #717171
|
||||
|
||||
pre
|
||||
max-height 120px
|
||||
font-size 80%
|
||||
|
||||
</style>
|
||||
|
||||
412
src/web/app/mobile/views/components/posts-post.vue
Normal file
412
src/web/app/mobile/views/components/posts-post.vue
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
<template>
|
||||
<div class="mk-posts-post" :class="{ repost: isRepost }">
|
||||
<div class="reply-to" v-if="p.reply">
|
||||
<mk-timeline-post-sub post={ p.reply }/>
|
||||
</div>
|
||||
<div class="repost" v-if="isRepost">
|
||||
<p>
|
||||
<a class="avatar-anchor" href={ '/' + post.user.username }>
|
||||
<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
|
||||
</a>
|
||||
%fa:retweet%{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}
|
||||
</p>
|
||||
<mk-time time={ post.created_at }/>
|
||||
</div>
|
||||
<article>
|
||||
<a class="avatar-anchor" href={ '/' + p.user.username }>
|
||||
<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/>
|
||||
</a>
|
||||
<div class="main">
|
||||
<header>
|
||||
<a class="name" href={ '/' + p.user.username }>{ p.user.name }</a>
|
||||
<span class="is-bot" v-if="p.user.is_bot">bot</span>
|
||||
<span class="username">@{ p.user.username }</span>
|
||||
<a class="created-at" href={ url }>
|
||||
<mk-time time={ p.created_at }/>
|
||||
</a>
|
||||
</header>
|
||||
<div class="body">
|
||||
<div class="text" ref="text">
|
||||
<p class="channel" v-if="p.channel != null"><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
|
||||
<a class="reply" v-if="p.reply">
|
||||
%fa:reply%
|
||||
</a>
|
||||
<p class="dummy"></p>
|
||||
<a class="quote" v-if="p.repost != null">RP:</a>
|
||||
</div>
|
||||
<div class="media" v-if="p.media">
|
||||
<mk-images images={ p.media }/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" post={ p } ref="pollViewer"/>
|
||||
<span class="app" v-if="p.app">via <b>{ p.app.name }</b></span>
|
||||
<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
|
||||
<mk-post-preview class="repost" post={ p.repost }/>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
|
||||
<button @click="reply">
|
||||
%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
|
||||
</button>
|
||||
<button @click="repost" title="Repost">
|
||||
%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
|
||||
</button>
|
||||
<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton">
|
||||
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
|
||||
</button>
|
||||
<button class="menu" @click="menu" ref="menuButton">
|
||||
%fa:ellipsis-h%
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import openPostForm from '../scripts/open-post-form';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
connectionId: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isRepost(): boolean {
|
||||
return (this.post.repost &&
|
||||
this.post.text == null &&
|
||||
this.post.media_ids == null &&
|
||||
this.post.poll == null);
|
||||
},
|
||||
p(): any {
|
||||
return this.isRepost ? this.post.repost : this.post;
|
||||
},
|
||||
reactionsCount(): number {
|
||||
return this.p.reaction_counts
|
||||
? Object.keys(this.p.reaction_counts)
|
||||
.map(key => this.p.reaction_counts[key])
|
||||
.reduce((a, b) => a + b)
|
||||
: 0;
|
||||
},
|
||||
url(): string {
|
||||
return `/${this.p.user.username}/${this.p.id}`;
|
||||
},
|
||||
urls(): string[] {
|
||||
if (this.p.ast) {
|
||||
return this.p.ast
|
||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
||||
.map(t => t.url);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.connection = this.$root.$data.os.stream.getConnection();
|
||||
this.connectionId = this.$root.$data.os.stream.use();
|
||||
},
|
||||
mounted() {
|
||||
this.capture(true);
|
||||
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection.on('_connected_', this.onStreamConnected);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.decapture(true);
|
||||
this.connection.off('_connected_', this.onStreamConnected);
|
||||
this.$root.$data.os.stream.dispose(this.connectionId);
|
||||
},
|
||||
methods: {
|
||||
capture(withHandler = false) {
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection.send({
|
||||
type: 'capture',
|
||||
id: this.post.id
|
||||
});
|
||||
if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
|
||||
}
|
||||
},
|
||||
decapture(withHandler = false) {
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection.send({
|
||||
type: 'decapture',
|
||||
id: this.post.id
|
||||
});
|
||||
if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
|
||||
}
|
||||
},
|
||||
onStreamConnected() {
|
||||
this.capture();
|
||||
},
|
||||
onStreamPostUpdated(data) {
|
||||
const post = data.post;
|
||||
if (post.id == this.post.id) {
|
||||
this.$emit('update:post', post);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-posts-post
|
||||
font-size 12px
|
||||
border-bottom solid 1px #eaeaea
|
||||
|
||||
&:first-child
|
||||
border-radius 8px 8px 0 0
|
||||
|
||||
> .repost
|
||||
border-radius 8px 8px 0 0
|
||||
|
||||
&:last-of-type
|
||||
border-bottom none
|
||||
|
||||
@media (min-width 350px)
|
||||
font-size 14px
|
||||
|
||||
@media (min-width 500px)
|
||||
font-size 16px
|
||||
|
||||
> .repost
|
||||
color #9dbb00
|
||||
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 8px 16px
|
||||
line-height 28px
|
||||
|
||||
@media (min-width 500px)
|
||||
padding 16px
|
||||
|
||||
.avatar-anchor
|
||||
display inline-block
|
||||
|
||||
.avatar
|
||||
vertical-align bottom
|
||||
width 28px
|
||||
height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
.name
|
||||
font-weight bold
|
||||
|
||||
> mk-time
|
||||
position absolute
|
||||
top 8px
|
||||
right 16px
|
||||
font-size 0.9em
|
||||
line-height 28px
|
||||
|
||||
@media (min-width 500px)
|
||||
top 16px
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
|
||||
> .reply-to
|
||||
background rgba(0, 0, 0, 0.0125)
|
||||
|
||||
> mk-post-preview
|
||||
background transparent
|
||||
|
||||
> article
|
||||
padding 14px 16px 9px 16px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
float left
|
||||
margin 0 10px 8px 0
|
||||
position -webkit-sticky
|
||||
position sticky
|
||||
top 62px
|
||||
|
||||
@media (min-width 500px)
|
||||
margin-right 16px
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
width 48px
|
||||
height 48px
|
||||
margin 0
|
||||
border-radius 6px
|
||||
vertical-align bottom
|
||||
|
||||
@media (min-width 500px)
|
||||
width 58px
|
||||
height 58px
|
||||
border-radius 8px
|
||||
|
||||
> .main
|
||||
float left
|
||||
width calc(100% - 58px)
|
||||
|
||||
@media (min-width 500px)
|
||||
width calc(100% - 74px)
|
||||
|
||||
> header
|
||||
display flex
|
||||
white-space nowrap
|
||||
|
||||
@media (min-width 500px)
|
||||
margin-bottom 2px
|
||||
|
||||
> .name
|
||||
display block
|
||||
margin 0 0.5em 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
color #777
|
||||
font-size 1em
|
||||
font-weight 700
|
||||
text-align left
|
||||
text-decoration none
|
||||
text-overflow ellipsis
|
||||
|
||||
&:hover
|
||||
text-decoration underline
|
||||
|
||||
> .is-bot
|
||||
text-align left
|
||||
margin 0 0.5em 0 0
|
||||
padding 1px 6px
|
||||
font-size 12px
|
||||
color #aaa
|
||||
border solid 1px #ddd
|
||||
border-radius 3px
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0.5em 0 0
|
||||
color #ccc
|
||||
|
||||
> .created-at
|
||||
margin-left auto
|
||||
font-size 0.9em
|
||||
color #c0c0c0
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
cursor default
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
overflow-wrap break-word
|
||||
font-size 1.1em
|
||||
color #717171
|
||||
|
||||
> .dummy
|
||||
display none
|
||||
|
||||
mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
> .channel
|
||||
margin 0
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color #717171
|
||||
|
||||
> .quote
|
||||
margin-left 4px
|
||||
font-style oblique
|
||||
color #a0bf46
|
||||
|
||||
code
|
||||
padding 4px 8px
|
||||
margin 0 0.5em
|
||||
font-size 80%
|
||||
color #525252
|
||||
background #f8f8f8
|
||||
border-radius 2px
|
||||
|
||||
pre > code
|
||||
padding 16px
|
||||
margin 0
|
||||
|
||||
[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
|
||||
|
||||
> .media
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
|
||||
> .app
|
||||
font-size 12px
|
||||
color #ccc
|
||||
|
||||
> mk-poll
|
||||
font-size 80%
|
||||
|
||||
> .repost
|
||||
margin 8px 0
|
||||
|
||||
> [data-fa]:first-child
|
||||
position absolute
|
||||
top -8px
|
||||
left -8px
|
||||
z-index 1
|
||||
color #c0dac6
|
||||
font-size 28px
|
||||
background #fff
|
||||
|
||||
> mk-post-preview
|
||||
padding 16px
|
||||
border dashed 1px #c0dac6
|
||||
border-radius 8px
|
||||
|
||||
> footer
|
||||
> button
|
||||
margin 0
|
||||
padding 8px
|
||||
background transparent
|
||||
border none
|
||||
box-shadow none
|
||||
font-size 1em
|
||||
color #ddd
|
||||
cursor pointer
|
||||
|
||||
&:not(:last-child)
|
||||
margin-right 28px
|
||||
|
||||
&:hover
|
||||
color #666
|
||||
|
||||
> .count
|
||||
display inline
|
||||
margin 0 0 0 8px
|
||||
color #999
|
||||
|
||||
&.reacted
|
||||
color $theme-color
|
||||
|
||||
&.menu
|
||||
@media (max-width 350px)
|
||||
display none
|
||||
|
||||
</style>
|
||||
|
||||
97
src/web/app/mobile/views/components/posts.vue
Normal file
97
src/web/app/mobile/views/components/posts.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<div class="mk-posts">
|
||||
<slot name="head"></slot>
|
||||
<template v-for="(post, i) in _posts">
|
||||
<mk-posts-post :post="post" :key="post.id"/>
|
||||
<p class="date" :key="post._datetext" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
|
||||
<span>%fa:angle-up%{{ post._datetext }}</span>
|
||||
<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
<slot name="tail"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
posts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_posts(): any[] {
|
||||
return (this.posts as any).map(post => {
|
||||
const date = new Date(post.created_at).getDate();
|
||||
const month = new Date(post.created_at).getMonth() + 1;
|
||||
post._date = date;
|
||||
post._datetext = `${month}月 ${date}日`;
|
||||
return post;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-posts
|
||||
background #fff
|
||||
border-radius 8px
|
||||
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> .init
|
||||
padding 64px 0
|
||||
text-align center
|
||||
color #999
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .empty
|
||||
margin 0 auto
|
||||
padding 32px
|
||||
max-width 400px
|
||||
text-align center
|
||||
color #999
|
||||
|
||||
> [data-fa]
|
||||
display block
|
||||
margin-bottom 16px
|
||||
font-size 3em
|
||||
color #ccc
|
||||
|
||||
> .date
|
||||
display block
|
||||
margin 0
|
||||
line-height 32px
|
||||
text-align center
|
||||
font-size 0.9em
|
||||
color #aaa
|
||||
background #fdfdfd
|
||||
border-bottom solid 1px #eaeaea
|
||||
|
||||
span
|
||||
margin 0 16px
|
||||
|
||||
[data-fa]
|
||||
margin-right 8px
|
||||
|
||||
> footer
|
||||
text-align center
|
||||
border-top solid 1px #eaeaea
|
||||
border-bottom-left-radius 4px
|
||||
border-bottom-right-radius 4px
|
||||
|
||||
> button
|
||||
margin 0
|
||||
padding 16px
|
||||
width 100%
|
||||
color $theme-color
|
||||
border-radius 0 0 8px 8px
|
||||
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
|
||||
</style>
|
||||
43
src/web/app/mobile/views/components/sub-post-content.vue
Normal file
43
src/web/app/mobile/views/components/sub-post-content.vue
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="mk-sub-post-content">
|
||||
<div class="body">
|
||||
<a class="reply" v-if="post.reply_id">%fa:reply%</a>
|
||||
<mk-post-html v-if="post.ast" :ast="post.ast" :i="$root.$data.os.i"/>
|
||||
<a class="quote" v-if="post.repost_id">RP: ...</a>
|
||||
</div>
|
||||
<details v-if="post.media">
|
||||
<summary>({{ post.media.length }}個のメディア)</summary>
|
||||
<mk-images :images="post.media"/>
|
||||
</details>
|
||||
<details v-if="post.poll">
|
||||
<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>
|
||||
<mk-poll :post="post"/>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['post']
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-sub-post-content
|
||||
overflow-wrap break-word
|
||||
|
||||
> .body
|
||||
> .reply
|
||||
margin-right 6px
|
||||
color #717171
|
||||
|
||||
> .quote
|
||||
margin-left 4px
|
||||
font-style oblique
|
||||
color #a0bf46
|
||||
|
||||
mk-poll
|
||||
font-size 80%
|
||||
|
||||
</style>
|
||||
89
src/web/app/mobile/views/components/timeline.vue
Normal file
89
src/web/app/mobile/views/components/timeline.vue
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<div class="mk-timeline">
|
||||
<mk-friends-maker v-if="alone"/>
|
||||
<mk-posts ref="timeline" :posts="posts">
|
||||
<div class="init" v-if="fetching">
|
||||
%fa:spinner .pulse%%i18n:common.loading%
|
||||
</div>
|
||||
<div class="empty" v-if="!fetching && posts.length == 0">
|
||||
%fa:R comments%
|
||||
%i18n:mobile.tags.mk-home-timeline.empty-timeline%
|
||||
</div>
|
||||
<button v-if="canFetchMore" @click="more" :disabled="fetching" slot="tail">
|
||||
<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
|
||||
<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
|
||||
</button>
|
||||
</mk-posts>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
date: {
|
||||
type: Date,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
posts: [],
|
||||
connection: null,
|
||||
connectionId: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
alone(): boolean {
|
||||
return this.$root.$data.os.i.following_count == 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connection = this.$root.$data.os.stream.getConnection();
|
||||
this.connectionId = this.$root.$data.os.stream.use();
|
||||
|
||||
this.connection.on('post', this.onPost);
|
||||
this.connection.on('follow', this.onChangeFollowing);
|
||||
this.connection.on('unfollow', this.onChangeFollowing);
|
||||
|
||||
this.fetch();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.connection.off('post', this.onPost);
|
||||
this.connection.off('follow', this.onChangeFollowing);
|
||||
this.connection.off('unfollow', this.onChangeFollowing);
|
||||
this.$root.$data.os.stream.dispose(this.connectionId);
|
||||
},
|
||||
methods: {
|
||||
fetch(cb?) {
|
||||
this.fetching = true;
|
||||
|
||||
this.$root.$data.os.api('posts/timeline', {
|
||||
until_date: this.date ? (this.date as any).getTime() : undefined
|
||||
}).then(posts => {
|
||||
this.fetching = false;
|
||||
this.posts = posts;
|
||||
if (cb) cb();
|
||||
});
|
||||
},
|
||||
more() {
|
||||
if (this.moreFetching || this.fetching || this.posts.length == 0) return;
|
||||
this.moreFetching = true;
|
||||
this.$root.$data.os.api('posts/timeline', {
|
||||
until_id: this.posts[this.posts.length - 1].id
|
||||
}).then(posts => {
|
||||
this.moreFetching = false;
|
||||
this.posts.unshift(posts);
|
||||
});
|
||||
},
|
||||
onPost(post) {
|
||||
this.posts.unshift(post);
|
||||
},
|
||||
onChangeFollowing() {
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
169
src/web/app/mobile/views/components/ui-header.vue
Normal file
169
src/web/app/mobile/views/components/ui-header.vue
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<div class="mk-ui-header">
|
||||
<mk-special-message/>
|
||||
<div class="main">
|
||||
<div class="backdrop"></div>
|
||||
<div class="content">
|
||||
<button class="nav" @click="parent.toggleDrawer">%fa:bars%</button>
|
||||
<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</template>
|
||||
<h1 ref="title">Misskey</h1>
|
||||
<button v-if="func" @click="func"><mk-raw content={ funcIcon }/></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
func: null,
|
||||
funcIcon: null,
|
||||
hasUnreadNotifications: false,
|
||||
hasUnreadMessagingMessages: false,
|
||||
connection: null,
|
||||
connectionId: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection = this.$root.$data.os.stream.getConnection();
|
||||
this.connectionId = this.$root.$data.os.stream.use();
|
||||
|
||||
this.connection.on('read_all_notifications', this.onReadAllNotifications);
|
||||
this.connection.on('unread_notification', this.onUnreadNotification);
|
||||
this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
// Fetch count of unread notifications
|
||||
this.$root.$data.os.api('notifications/get_unread_count').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.hasUnreadNotifications = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch count of unread messaging messages
|
||||
this.$root.$data.os.api('messaging/unread').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.hasUnreadMessagingMessages = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection.off('read_all_notifications', this.onReadAllNotifications);
|
||||
this.connection.off('unread_notification', this.onUnreadNotification);
|
||||
this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
this.$root.$data.os.stream.dispose(this.connectionId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setFunc(fn, icon) {
|
||||
this.func = fn;
|
||||
this.funcIcon = icon;
|
||||
},
|
||||
onReadAllNotifications() {
|
||||
this.hasUnreadNotifications = false;
|
||||
},
|
||||
onUnreadNotification() {
|
||||
this.hasUnreadNotifications = true;
|
||||
},
|
||||
onReadAllMessagingMessages() {
|
||||
this.hasUnreadMessagingMessages = false;
|
||||
},
|
||||
onUnreadMessagingMessage() {
|
||||
this.hasUnreadMessagingMessages = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-ui-header
|
||||
$height = 48px
|
||||
|
||||
position fixed
|
||||
top 0
|
||||
z-index 1024
|
||||
width 100%
|
||||
box-shadow 0 1px 0 rgba(#000, 0.075)
|
||||
|
||||
> .main
|
||||
color rgba(#fff, 0.9)
|
||||
|
||||
> .backdrop
|
||||
position absolute
|
||||
top 0
|
||||
z-index 1023
|
||||
width 100%
|
||||
height $height
|
||||
-webkit-backdrop-filter blur(12px)
|
||||
backdrop-filter blur(12px)
|
||||
background-color rgba(#1b2023, 0.75)
|
||||
|
||||
> .content
|
||||
z-index 1024
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 0
|
||||
width 100%
|
||||
max-width calc(100% - 112px)
|
||||
text-align center
|
||||
font-size 1.1em
|
||||
font-weight normal
|
||||
line-height $height
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
[data-fa]
|
||||
margin-right 8px
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
vertical-align bottom
|
||||
width ($height - 16px)
|
||||
height ($height - 16px)
|
||||
margin 8px
|
||||
border-radius 6px
|
||||
|
||||
> .nav
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width $height
|
||||
font-size 1.4em
|
||||
line-height $height
|
||||
border-right solid 1px rgba(#000, 0.1)
|
||||
|
||||
> [data-fa]
|
||||
transition all 0.2s ease
|
||||
|
||||
> [data-fa].circle
|
||||
position absolute
|
||||
top 8px
|
||||
left 8px
|
||||
pointer-events none
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
> button:last-child
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
width $height
|
||||
text-align center
|
||||
font-size 1.4em
|
||||
color inherit
|
||||
line-height $height
|
||||
border-left solid 1px rgba(#000, 0.1)
|
||||
|
||||
</style>
|
||||
196
src/web/app/mobile/views/components/ui-nav.vue
Normal file
196
src/web/app/mobile/views/components/ui-nav.vue
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<div class="mk-ui-nav" :style="{ display: isOpen ? 'block' : 'none' }">
|
||||
<div class="backdrop" @click="parent.toggleDrawer"></div>
|
||||
<div class="body">
|
||||
<a class="me" v-if="SIGNIN" href={ '/' + I.username }>
|
||||
<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
|
||||
<p class="name">{ I.name }</p>
|
||||
</a>
|
||||
<div class="links">
|
||||
<ul>
|
||||
<li><a href="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</a></li>
|
||||
<li><a href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</a></li>
|
||||
<li><a href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href={ _CH_URL_ } target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li>
|
||||
<li><a href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a @click="search">%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href={ aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
hasUnreadNotifications: false,
|
||||
hasUnreadMessagingMessages: false,
|
||||
connection: null,
|
||||
connectionId: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection = this.$root.$data.os.stream.getConnection();
|
||||
this.connectionId = this.$root.$data.os.stream.use();
|
||||
|
||||
this.connection.on('read_all_notifications', this.onReadAllNotifications);
|
||||
this.connection.on('unread_notification', this.onUnreadNotification);
|
||||
this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
|
||||
// Fetch count of unread notifications
|
||||
this.$root.$data.os.api('notifications/get_unread_count').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.hasUnreadNotifications = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch count of unread messaging messages
|
||||
this.$root.$data.os.api('messaging/unread').then(res => {
|
||||
if (res.count > 0) {
|
||||
this.hasUnreadMessagingMessages = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection.off('read_all_notifications', this.onReadAllNotifications);
|
||||
this.connection.off('unread_notification', this.onUnreadNotification);
|
||||
this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
|
||||
this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
|
||||
this.$root.$data.os.stream.dispose(this.connectionId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search() {
|
||||
const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
|
||||
if (query == null || query == '') return;
|
||||
this.page('/search?q=' + encodeURIComponent(query));
|
||||
},
|
||||
onReadAllNotifications() {
|
||||
this.hasUnreadNotifications = false;
|
||||
},
|
||||
onUnreadNotification() {
|
||||
this.hasUnreadNotifications = true;
|
||||
},
|
||||
onReadAllMessagingMessages() {
|
||||
this.hasUnreadMessagingMessages = false;
|
||||
},
|
||||
onUnreadMessagingMessage() {
|
||||
this.hasUnreadMessagingMessages = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-ui-nav
|
||||
.backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1025
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.2)
|
||||
|
||||
.body
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 1026
|
||||
width 240px
|
||||
height 100%
|
||||
overflow auto
|
||||
-webkit-overflow-scrolling touch
|
||||
color #777
|
||||
background #fff
|
||||
|
||||
.me
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
|
||||
.avatar
|
||||
display inline
|
||||
max-width 64px
|
||||
border-radius 32px
|
||||
vertical-align middle
|
||||
|
||||
.name
|
||||
display block
|
||||
margin 0 16px
|
||||
position absolute
|
||||
top 0
|
||||
left 80px
|
||||
padding 0
|
||||
width calc(100% - 112px)
|
||||
color #777
|
||||
line-height 96px
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
|
||||
ul
|
||||
display block
|
||||
margin 16px 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
li
|
||||
display block
|
||||
font-size 1em
|
||||
line-height 1em
|
||||
|
||||
a
|
||||
display block
|
||||
padding 0 20px
|
||||
line-height 3rem
|
||||
line-height calc(1rem + 30px)
|
||||
color #777
|
||||
text-decoration none
|
||||
|
||||
> [data-fa]:first-child
|
||||
margin-right 0.5em
|
||||
|
||||
> [data-fa].circle
|
||||
margin-left 6px
|
||||
font-size 10px
|
||||
color $theme-color
|
||||
|
||||
> [data-fa]:last-child
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 0 20px
|
||||
font-size 1.2em
|
||||
line-height calc(1rem + 30px)
|
||||
color #ccc
|
||||
|
||||
.about
|
||||
margin 0
|
||||
padding 1em 0
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
opacity 0.5
|
||||
|
||||
a
|
||||
color #777
|
||||
|
||||
</style>
|
||||
57
src/web/app/mobile/views/components/ui.vue
Normal file
57
src/web/app/mobile/views/components/ui.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="mk-ui">
|
||||
<mk-ui-header/>
|
||||
<mk-ui-nav :is-open="isDrawerOpening"/>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<mk-stream-indicator v-if="$root.$data.os.isSignedIn"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
isDrawerOpening: false,
|
||||
connection: null,
|
||||
connectionId: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection = this.$root.$data.os.stream.getConnection();
|
||||
this.connectionId = this.$root.$data.os.stream.use();
|
||||
|
||||
this.connection.on('notification', this.onNotification);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$root.$data.os.isSignedIn) {
|
||||
this.connection.off('notification', this.onNotification);
|
||||
this.$root.$data.os.stream.dispose(this.connectionId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onNotification(notification) {
|
||||
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
|
||||
this.connection.send({
|
||||
type: 'read_notification',
|
||||
id: notification.id
|
||||
});
|
||||
|
||||
document.body.appendChild(new MkNotify({
|
||||
propsData: {
|
||||
notification
|
||||
}
|
||||
}).$mount().$el);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-ui
|
||||
padding-top 48px
|
||||
</style>
|
||||
62
src/web/app/mobile/views/components/user-card.vue
Normal file
62
src/web/app/mobile/views/components/user-card.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<div class="mk-user-card">
|
||||
<header :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''">
|
||||
<a :href="`/${user.username}`">
|
||||
<img :src="`${user.avatar_url}?thumbnail&size=200`" alt="avatar"/>
|
||||
</a>
|
||||
</header>
|
||||
<a class="name" :href="`/${user.username}`" target="_blank">{{ user.name }}</a>
|
||||
<p class="username">@{{ user.username }}</p>
|
||||
<mk-follow-button :user="user"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['user']
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-user-card
|
||||
display inline-block
|
||||
width 200px
|
||||
text-align center
|
||||
border-radius 8px
|
||||
background #fff
|
||||
|
||||
> header
|
||||
display block
|
||||
height 80px
|
||||
background-color #ddd
|
||||
background-size cover
|
||||
background-position center
|
||||
border-radius 8px 8px 0 0
|
||||
|
||||
> a
|
||||
> img
|
||||
position absolute
|
||||
top 20px
|
||||
left calc(50% - 40px)
|
||||
width 80px
|
||||
height 80px
|
||||
border solid 2px #fff
|
||||
border-radius 8px
|
||||
|
||||
> .name
|
||||
display block
|
||||
margin 24px 0 0 0
|
||||
font-size 16px
|
||||
color #555
|
||||
|
||||
> .username
|
||||
margin 0
|
||||
font-size 15px
|
||||
color #ccc
|
||||
|
||||
> mk-follow-button
|
||||
display inline-block
|
||||
margin 8px 0 16px 0
|
||||
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue