[Client] Messagingをいろいろ

This commit is contained in:
syuilo
2017-02-15 15:45:01 +09:00
parent 4c498320c2
commit 3d38356a91
14 changed files with 223 additions and 132 deletions

View File

@@ -20,3 +20,7 @@ require('./twitter-setting.tag');
require('./authorized-apps.tag');
require('./poll.tag');
require('./poll-editor.tag');
require('./messaging/room.tag');
require('./messaging/message.tag');
require('./messaging/index.tag');
require('./messaging/form.tag');

View File

@@ -0,0 +1,159 @@
<mk-messaging-form>
<textarea ref="text" onkeypress={ onkeypress } onpaste={ onpaste } placeholder="ここにメッセージを入力"></textarea>
<div class="files"></div>
<mk-uploader ref="uploader"></mk-uploader>
<button class="send" onclick={ send } disabled={ sending } title="メッセージを送信"><i class="fa fa-paper-plane" if={ !sending }></i><i class="fa fa-spinner fa-spin" if={ sending }></i></button>
<button class="attach-from-local" type="button" title="PCから画像を添付する"><i class="fa fa-upload"></i></button>
<button class="attach-from-drive" type="button" title="アルバムから画像を添付する"><i class="fa fa-folder-open"></i></button>
<input name="file" type="file" accept="image/*"/>
<style type="stylus">
:scope
display block
> textarea
cursor auto
display block
width 100%
min-width 100%
max-width 100%
height 64px
margin 0
padding 8px
font-size 1em
color #000
outline none
border none
border-top solid 1px #eee
border-radius 0
box-shadow none
background transparent
> .send
position absolute
bottom 0
right 0
margin 0
padding 10px 14px
line-height 1em
font-size 1em
color #aaa
transition color 0.1s ease
&:hover
color $theme-color
&:active
color darken($theme-color, 10%)
transition color 0s ease
.files
display block
margin 0
padding 0 8px
list-style none
&:after
content ''
display block
clear both
> li
display block
float left
margin 4px
padding 0
width 64px
height 64px
background-color #eee
background-repeat no-repeat
background-position center center
background-size cover
cursor move
&:hover
> .remove
display block
> .remove
display none
position absolute
right -6px
top -6px
margin 0
padding 0
background transparent
outline none
border none
border-radius 0
box-shadow none
cursor pointer
.attach-from-local
.attach-from-drive
margin 0
padding 10px 14px
line-height 1em
font-size 1em
font-weight normal
text-decoration none
color #aaa
transition color 0.1s ease
&:hover
color $theme-color
&:active
color darken($theme-color, 10%)
transition color 0s ease
input[type=file]
display none
</style>
<script>
@mixin \api
@onpaste = (e) ~>
data = e.clipboard-data
items = data.items
for i from 0 to items.length - 1
item = items[i]
switch (item.kind)
| \file =>
@upload item.get-as-file!
@onkeypress = (e) ~>
if (e.which == 10 || e.which == 13) && e.ctrl-key
@send!
@select-file = ~>
@refs.file.click!
@select-file-from-drive = ~>
browser = document.body.append-child document.create-element \mk-select-file-from-drive-window
event = riot.observable!
riot.mount browser, do
multiple: true
event: event
event.one \selected (files) ~>
files.for-each @add-file
@send = ~>
@sending = true
@api \messaging/messages/create do
user_id: @opts.user.id
text: @refs.text.value
.then (message) ~>
@clear!
.catch (err) ~>
console.error err
.then ~>
@sending = false
@update!
@clear = ~>
@refs.text.value = ''
@files = []
@update!
</script>
</mk-messaging-form>

View File

@@ -0,0 +1,328 @@
<mk-messaging>
<div class="search">
<div class="form">
<label for="search-input"><i class="fa fa-search"></i></label>
<input ref="searchInput" type="search" oninput={ search } placeholder="ユーザーを探す"/>
</div>
<div class="result">
<ol class="users" if={ searchResult.length > 0 }>
<li each={ user in searchResult }>
<a onclick={ user._click }>
<img class="avatar" src={ user.avatar_url + '?thumbnail&size=32' } alt=""/>
<span class="name">{ user.name }</span>
<span class="username">@{ user.username }</span>
</a>
</li>
</ol>
</div>
</div>
<div class="history" if={ history.length > 0 }>
<virtual each={ history }>
<a class="user" data-is-me={ is_me } data-is-read={ is_read } onclick={ _click }>
<div>
<img class="avatar" src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' } alt=""/>
<header>
<span class="name">{ is_me ? recipient.name : user.name }</span>
<span class="username">{ '@' + (is_me ? recipient.username : user.username ) }</span>
<mk-time time={ created_at }></mk-time>
</header>
<div class="body">
<p class="text"><span class="me" if={ is_me }>あなた:</span>{ text }</p>
</div>
</div>
</a>
</virtual>
</div>
<p class="no-history" if={ history.length == 0 }>履歴はありません。<br/>ユーザーを検索して、いつでもメッセージを送受信できます。</p>
<style type="stylus">
:scope
display block
> .search
display block
position -webkit-sticky
position sticky
top 0
left 0
z-index 1
width 100%
background #fff
box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
> .form
padding 8px
background #f7f7f7
> label
display block
position absolute
top 0
left 8px
z-index 1
height 100%
width 38px
pointer-events none
> i
display block
position absolute
top 0
right 0
bottom 0
left 0
width 1em
height 1em
margin auto
color #555
> input
margin 0
padding 0 12px 0 38px
width 100%
font-size 1em
line-height 38px
color #000
outline none
border solid 1px #eee
border-radius 5px
box-shadow none
transition color 0.5s ease, border 0.5s ease
&:hover
border solid 1px #ddd
transition border 0.2s ease
&:focus
color darken($theme-color, 20%)
border solid 1px $theme-color
transition color 0, border 0
> .result
display block
top 0
left 0
z-index 2
width 100%
margin 0
padding 0
background #fff
> .users
margin 0
padding 0
list-style none
> li
> a
display inline-block
z-index 1
width 100%
padding 8px 32px
vertical-align top
white-space nowrap
overflow hidden
color rgba(0, 0, 0, 0.8)
text-decoration none
transition none
&:hover
color #fff
background $theme-color
.name
color #fff
.username
color #fff
&:active
color #fff
background darken($theme-color, 10%)
.name
color #fff
.username
color #fff
.avatar
vertical-align middle
min-width 32px
min-height 32px
max-width 32px
max-height 32px
margin 0 8px 0 0
border-radius 6px
.name
margin 0 8px 0 0
/*font-weight bold*/
font-weight normal
color rgba(0, 0, 0, 0.8)
.username
font-weight normal
color rgba(0, 0, 0, 0.3)
> .history
> a
display block
padding 20px 30px
text-decoration none
background #fff
border-bottom solid 1px #eee
*
pointer-events none
user-select none
&:hover
background #fafafa
> .avatar
filter saturate(200%)
&:active
background #eee
&[data-is-read]
&[data-is-me]
opacity 0.8
&:not([data-is-me]):not([data-is-read])
background-image url("/_/resources/desktop/unread.svg")
background-repeat no-repeat
background-position 0 center
&:after
content ""
display block
clear both
> div
max-width 500px
margin 0 auto
> header
margin-bottom 2px
white-space nowrap
overflow hidden
> .name
text-align left
display inline
margin 0
padding 0
font-size 1em
color rgba(0, 0, 0, 0.9)
font-weight bold
transition all 0.1s ease
> .username
text-align left
margin 0 0 0 8px
color rgba(0, 0, 0, 0.5)
> mk-time
position absolute
top 0
right 0
display inline
color rgba(0, 0, 0, 0.5)
font-size 80%
> .avatar
float left
width 54px
height 54px
margin 0 16px 0 0
border-radius 8px
transition all 0.1s ease
> .body
> .text
display block
margin 0 0 0 0
padding 0
overflow hidden
overflow-wrap break-word
font-size 1.1em
color rgba(0, 0, 0, 0.8)
.me
color rgba(0, 0, 0, 0.4)
> .image
display block
max-width 100%
max-height 512px
> .no-history
margin 0
padding 2em 1em
text-align center
color #999
font-weight 500
// TODO: element base media query
@media (max-width 400px)
> .search
> .result
> .users
> li
> a
padding 8px 16px
> .history
> a
padding 16px
font-size 14px
> div
> .avatar
margin 0 12px 0 0
</style>
<script>
@mixin \i
@mixin \api
@search-result = []
@on \mount ~>
@api \messaging/history
.then (history) ~>
@is-loading = false
history.for-each (message) ~>
message.is_me = message.user_id == @I.id
message._click = ~>
if message.is_me
@trigger \navigate-user message.recipient
else
@trigger \navigate-user message.user
@history = history
@update!
.catch (err) ~>
console.error err
@search = ~>
q = @refs.search-input.value
if q == ''
@search-result = []
else
@api \users/search do
query: q
.then (users) ~>
users.for-each (user) ~>
user._click = ~>
@trigger \navigate-user user
@search-result = []
@search-result = users
@update!
.catch (err) ~>
console.error err
</script>
</mk-messaging>

View File

@@ -0,0 +1,230 @@
<mk-messaging-message data-is-me={ message.is_me }><a class="avatar-anchor" href={ CONFIG.url + '/' + message.user.username } title={ message.user.username } target="_blank"><img class="avatar" src={ message.user.avatar_url + '?thumbnail&size=64' } alt=""/></a>
<div class="content-container">
<div class="balloon">
<p class="read" if={ message.is_me && message.is_read }>既読</p>
<button class="delete-button" if={ message.is_me } title="メッセージを削除"><img src="/_/resources/desktop/messaging/delete.png" alt="Delete"/></button>
<div class="content" if={ !message.is_deleted }>
<div ref="text"></div>
<div class="image" if={ message.file }><img src={ message.file.url } alt="image" title={ message.file.name }/></div>
</div>
<div class="content" if={ message.is_deleted }>
<p class="is-deleted">このメッセージは削除されました</p>
</div>
</div>
<footer>
<mk-time time={ message.created_at }></mk-time><i class="fa fa-pencil is-edited" if={ message.is_edited }></i>
</footer>
</div>
<style type="stylus">
:scope
$me-balloon-color = #23A7B6
display block
padding 10px 12px 10px 12px
background-color transparent
&:after
content ""
display block
clear both
> .avatar-anchor
display block
> .avatar
display block
min-width 54px
min-height 54px
max-width 54px
max-height 54px
margin 0
border-radius 8px
transition all 0.1s ease
> .content-container
display block
margin 0 12px
padding 0
max-width calc(100% - 78px)
> .balloon
display block
float inherit
margin 0
padding 0
max-width 100%
min-height 38px
border-radius 16px
&:before
content ""
pointer-events none
display block
position absolute
top 12px
&:hover
> .delete-button
display block
> .delete-button
display none
position absolute
z-index 1
top -4px
right -4px
margin 0
padding 0
cursor pointer
outline none
border none
border-radius 0
box-shadow none
background transparent
> img
vertical-align bottom
width 16px
height 16px
cursor pointer
> .read
user-select none
display block
position absolute
z-index 1
bottom -4px
left -12px
margin 0
color rgba(0, 0, 0, 0.5)
font-size 11px
> .content
> .is-deleted
display block
margin 0
padding 0
overflow hidden
overflow-wrap break-word
font-size 1em
color rgba(0, 0, 0, 0.5)
> [ref='text']
display block
margin 0
padding 8px 16px
overflow hidden
overflow-wrap break-word
font-size 1em
color rgba(0, 0, 0, 0.8)
&, *
user-select text
cursor auto
& + .file
&.image
> img
border-radius 0 0 16px 16px
> .file
&.image
> img
display block
max-width 100%
max-height 512px
border-radius 16px
> footer
display block
clear both
margin 0
padding 2px
font-size 10px
color rgba(0, 0, 0, 0.4)
> .is-edited
margin-left 4px
&:not([data-is-me='true'])
> .avatar-anchor
float left
> .content-container
float left
> .balloon
background #eee
&:before
left -14px
border-top solid 8px transparent
border-right solid 8px #eee
border-bottom solid 8px transparent
border-left solid 8px transparent
> footer
text-align left
&[data-is-me='true']
> .avatar-anchor
float right
> .content-container
float right
> .balloon
background $me-balloon-color
&:before
right -14px
left auto
border-top solid 8px transparent
border-right solid 8px transparent
border-bottom solid 8px transparent
border-left solid 8px $me-balloon-color
> .content
> p.is-deleted
color rgba(255, 255, 255, 0.5)
> [ref='text']
&, *
color #fff !important
> footer
text-align right
&[data-is-deleted='true']
> .content-container
opacity 0.5
</style>
<script>
@mixin \i
@mixin \text
@message = @opts.message
@message.is_me = @message.user.id == @I.id
@on \mount ~>
if @message.text?
tokens = @analyze @message.text
@refs.text.innerHTML = @compile tokens
@refs.text.children.for-each (e) ~>
if e.tag-name == \MK-URL
riot.mount e
# URLをプレビュー
tokens
.filter (t) -> t.type == \link
.map (t) ~>
@preview = @refs.text.append-child document.create-element \mk-url-preview
riot.mount @preview, do
url: t.content
</script>
</mk-messaging-message>

View File

@@ -0,0 +1,224 @@
<mk-messaging-room>
<div class="stream" ref="stream">
<p class="initializing" if={ init }><i class="fa fa-spinner fa-spin"></i>読み込み中</p>
<p class="empty" if={ !init && messages.length == 0 }><i class="fa fa-info-circle"></i>このユーザーとまだ会話したことがありません</p>
<virtual each={ message, i in messages }>
<mk-messaging-message message={ message }></mk-messaging-message>
<p class="date" if={ i != messages.length - 1 && message._date != messages[i + 1]._date }><span>{ messages[i + 1]._datetext }</span></p>
</virtual>
</div>
<div class="typings"></div>
<footer>
<div ref="notifications"></div>
<div class="grippie" title="ドラッグしてフォームの広さを調整"></div>
<mk-messaging-form user={ user }></mk-messaging-form>
</footer>
<style type="stylus">
:scope
display block
> .stream
max-width 600px
margin 0 auto
> .empty
width 100%
margin 0
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
color rgba(0, 0, 0, 0.4)
i
margin-right 4px
> .no-history
display block
margin 0
padding 16px
text-align center
font-size 0.8em
color rgba(0, 0, 0, 0.4)
i
margin-right 4px
> .message
// something
> .date
display block
margin 8px 0
text-align center
&:before
content ''
display block
position absolute
height 1px
width 90%
top 16px
left 0
right 0
margin 0 auto
background rgba(0, 0, 0, 0.1)
> span
display inline-block
margin 0
padding 0 16px
//font-weight bold
line-height 32px
color rgba(0, 0, 0, 0.3)
background #fff
> footer
position -webkit-sticky
position sticky
z-index 2
bottom 0
width 100%
max-width 600px
margin 0 auto
padding 0
background rgba(255, 255, 255, 0.95)
background-clip content-box
> [ref='notifications']
position absolute
top -48px
width 100%
padding 8px 0
text-align center
> p
display inline-block
margin 0
padding 0 12px 0 28px
cursor pointer
line-height 32px
font-size 12px
color $theme-color-foreground
background $theme-color
border-radius 16px
transition opacity 1s ease
> i
position absolute
top 0
left 10px
line-height 32px
font-size 16px
> .grippie
height 10px
margin-top -10px
background transparent
cursor ns-resize
&:hover
//background rgba(0, 0, 0, 0.1)
&:active
//background rgba(0, 0, 0, 0.2)
</style>
<script>
@mixin \i
@mixin \api
@mixin \messaging-stream
@user = @opts.user
@init = true
@sending = false
@messages = []
@connection = new @MessagingStreamConnection @I, @user.id
@on \mount ~>
@connection.event.on \message @on-message
@connection.event.on \read @on-read
document.add-event-listener \visibilitychange @on-visibilitychange
@api \messaging/messages do
user_id: @user.id
.then (messages) ~>
@init = false
@messages = messages.reverse!
@update!
@scroll-to-bottom!
.catch (err) ~>
console.error err
@on \unmount ~>
@connection.event.off \message @on-message
@connection.event.off \read @on-read
@connection.close!
document.remove-event-listener \visibilitychange @on-visibilitychange
@on \update ~>
@messages.for-each (message) ~>
date = (new Date message.created_at).get-date!
month = (new Date message.created_at).get-month! + 1
message._date = date
message._datetext = month + '月 ' + date + '日'
@on-message = (message) ~>
is-bottom = @is-bottom!
@messages.push message
if message.user_id != @I.id and not document.hidden
@connection.socket.send JSON.stringify do
type: \read
id: message.id
@update!
if is-bottom
# Scroll to bottom
@scroll-to-bottom!
else if message.user_id != @I.id
# Notify
@notify '新しいメッセージがあります'
@on-read = (ids) ~>
if not Array.isArray ids then ids = [ids]
ids.for-each (id) ~>
if (@messages.some (x) ~> x.id == id)
exist = (@messages.map (x) -> x.id).index-of id
@messages[exist].is_read = true
@update!
@is-bottom = ~>
current = @refs.stream.scroll-top + @refs.stream.offset-height
max = @refs.stream.scroll-height
current > (max - 32)
@scroll-to-bottom = ~>
@refs.stream.scroll-top = @refs.stream.scroll-height
@notify = (message) ~>
n = document.create-element \p
n.inner-HTML = '<i class="fa fa-arrow-circle-down"></i>' + message
n.onclick = ~>
@scroll-to-bottom!
n.parent-node.remove-child n
@refs.notifications.append-child n
set-timeout ~>
n.style.opacity = 0
set-timeout ~>
n.parent-node.remove-child n
, 1000ms
, 4000ms
@on-visibilitychange = ~>
if document.hidden then return
@messages.for-each (message) ~>
if message.user_id != @I.id and not message.is_read
@connection.socket.send JSON.stringify do
type: \read
id: message.id
</script>
</mk-messaging-room>