Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
converse.js
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
converse.js
Commits
09a79d60
Commit
09a79d60
authored
Jun 03, 2020
by
JC Brand
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Create an image picker component and use it in the profile modal
parent
c82e3e9b
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
111 additions
and
64 deletions
+111
-64
.eslintrc.json
.eslintrc.json
+1
-0
src/components/image_picker.js
src/components/image_picker.js
+46
-0
src/components/message.js
src/components/message.js
+14
-2
src/converse-profile.js
src/converse-profile.js
+18
-25
src/templates/directives/avatar.js
src/templates/directives/avatar.js
+21
-23
src/templates/profile_modal.js
src/templates/profile_modal.js
+11
-14
No files found.
.eslintrc.json
View file @
09a79d60
...
...
@@ -198,6 +198,7 @@
"no-unused-expressions"
:
"off"
,
"no-use-before-define"
:
"off"
,
"no-useless-call"
:
"error"
,
"no-useless-catch"
:
"off"
,
"no-useless-computed-key"
:
"error"
,
"no-useless-concat"
:
"off"
,
"no-useless-constructor"
:
"error"
,
...
...
src/components/image_picker.js
0 → 100644
View file @
09a79d60
import
{
CustomElement
}
from
'
./element.js
'
;
import
{
__
}
from
'
@converse/headless/i18n
'
;
import
{
html
}
from
'
lit-element
'
;
import
{
renderAvatar
}
from
"
../templates/directives/avatar.js
"
;
const
i18n_alt_avatar
=
__
(
'
Your avatar image
'
);
export
class
ImagePicker
extends
CustomElement
{
static
get
properties
()
{
return
{
'
height
'
:
{
type
:
Number
},
'
image
'
:
{
type
:
String
},
'
width
'
:
{
type
:
Number
},
}
}
render
()
{
const
avatar_data
=
{
'
height
'
:
this
.
height
,
'
image
'
:
this
.
image
,
'
width
'
:
this
.
width
,
};
return
html
`
<a class="change-avatar" @click=
${
this
.
openFileSelection
}
title="
${
i18n_alt_avatar
}
">
${
renderAvatar
(
avatar_data
)
}
</a>
<input @change=
${
this
.
updateFilePreview
}
class="hidden" name="image" type="file"/>
`
;
}
openFileSelection
(
ev
)
{
ev
.
preventDefault
();
this
.
querySelector
(
'
input[type="file"]
'
).
click
();
}
updateFilePreview
(
ev
)
{
const
file
=
ev
.
target
.
files
[
0
];
const
reader
=
new
FileReader
();
reader
.
onloadend
=
()
=>
(
this
.
image
=
reader
.
result
);
reader
.
readAsDataURL
(
file
);
}
}
window
.
customElements
.
define
(
'
converse-image-picker
'
,
ImagePicker
);
src/components/message.js
View file @
09a79d60
...
...
@@ -115,7 +115,7 @@ class Message extends CustomElement {
const
size
=
filesize
(
this
.
model
.
file
.
size
);
return
html
`
<div class="message chat-msg">
${
renderAvatar
(
this
)
}
${
renderAvatar
(
this
.
getAvatarData
()
)
}
<div class="chat-msg__content">
<span class="chat-msg__text">
${
i18n_uploading
}
<strong>
${
filename
}
</strong>,
${
size
}
</span>
<progress value="
${
this
.
progress
}
"/>
...
...
@@ -132,7 +132,7 @@ class Message extends CustomElement {
${
this
.
isFollowup
()
?
'
chat-msg--followup
'
:
''
}
"
data-isodate="
${
this
.
time
}
" data-msgid="
${
this
.
msgid
}
" data-from="
${
this
.
from
}
" data-encrypted="
${
this
.
is_encrypted
}
">
${
renderAvatar
(
this
)
}
${
(
this
.
is_me_message
||
this
.
type
===
'
headline
'
)
?
''
:
renderAvatar
(
this
.
getAvatarData
()
)
}
<div class="chat-msg__content chat-msg__content--
${
this
.
sender
}
${
this
.
is_me_message
?
'
chat-msg__content--action
'
:
''
}
">
<span class="chat-msg__heading">
${
(
this
.
is_me_message
)
?
html
`
...
...
@@ -165,6 +165,18 @@ class Message extends CustomElement {
</div>`
;
}
getAvatarData
()
{
const
image_type
=
this
.
model
.
vcard
.
get
(
'
image_type
'
);
const
image_data
=
this
.
model
.
vcard
.
get
(
'
image
'
);
const
image
=
"
data:
"
+
image_type
+
"
;base64,
"
+
image_data
;
return
{
'
classes
'
:
'
chat-msg__avatar
'
,
'
height
'
:
36
,
'
width
'
:
36
,
image
,
};
}
async
onRetryClicked
()
{
this
.
show_spinner
=
true
;
await
this
.
model
.
error
.
retry
();
...
...
src/converse-profile.js
View file @
09a79d60
...
...
@@ -38,8 +38,6 @@ converse.plugins.add('converse-profile', {
_converse
.
ProfileModal
=
BootstrapModal
.
extend
({
id
:
"
user-profile-modal
"
,
events
:
{
'
change input[type="file"
'
:
"
updateFilePreview
"
,
'
click .change-avatar
'
:
"
openFileSelection
"
,
'
submit .profile-form
'
:
'
onFormSubmitted
'
},
...
...
@@ -58,29 +56,25 @@ converse.plugins.add('converse-profile', {
toHTML
()
{
return
tpl_profile_modal
(
Object
.
assign
(
this
.
model
.
toJSON
(),
this
.
model
.
vcard
.
toJSON
(),
{
'
_converse
'
:
_converse
,
'
utils
'
:
u
,
'
view
'
:
this
}));
},
afterRender
()
{
this
.
tabs
=
sizzle
(
'
.nav-item .nav-link
'
,
this
.
el
).
map
(
e
=>
new
bootstrap
.
Tab
(
e
));
this
.
model
.
vcard
.
toJSON
(),
this
.
getAvatarData
(),
{
'
view
'
:
this
}
));
},
openFileSelection
(
ev
)
{
ev
.
preventDefault
();
this
.
el
.
querySelector
(
'
input[type="file"]
'
).
click
();
getAvatarData
()
{
const
image_type
=
this
.
model
.
vcard
.
get
(
'
image_type
'
);
const
image_data
=
this
.
model
.
vcard
.
get
(
'
image
'
);
const
image
=
"
data:
"
+
image_type
+
"
;base64,
"
+
image_data
;
return
{
'
height
'
:
128
,
'
width
'
:
128
,
image
,
};
},
updateFilePreview
(
ev
)
{
const
file
=
ev
.
target
.
files
[
0
],
reader
=
new
FileReader
();
reader
.
onloadend
=
()
=>
{
this
.
el
.
querySelector
(
'
.avatar
'
).
setAttribute
(
'
src
'
,
reader
.
result
);
};
reader
.
readAsDataURL
(
file
);
afterRender
()
{
this
.
tabs
=
sizzle
(
'
.nav-item .nav-link
'
,
this
.
el
).
map
(
e
=>
new
bootstrap
.
Tab
(
e
));
},
async
setVCard
(
data
)
{
...
...
@@ -99,10 +93,9 @@ converse.plugins.add('converse-profile', {
onFormSubmitted
(
ev
)
{
ev
.
preventDefault
();
const
reader
=
new
FileReader
(),
form_data
=
new
FormData
(
ev
.
target
),
image_file
=
form_data
.
get
(
'
image
'
);
const
reader
=
new
FileReader
();
const
form_data
=
new
FormData
(
ev
.
target
);
const
image_file
=
form_data
.
get
(
'
image
'
);
const
data
=
{
'
fn
'
:
form_data
.
get
(
'
fn
'
),
'
nickname
'
:
form_data
.
get
(
'
nickname
'
),
...
...
src/templates/directives/avatar.js
View file @
09a79d60
import
tpl_avatar
from
"
templates/avatar.svg
"
;
import
xss
from
"
xss/dist/xss
"
;
import
{
directive
,
html
}
from
"
lit-html
"
;
import
{
unsafe
HTML
}
from
'
lit-html/directives/unsafe-html
.js
'
;
import
{
unsafe
SVG
}
from
'
lit-html/directives/unsafe-svg
.js
'
;
export
const
renderAvatar
=
directive
(
o
=>
part
=>
{
if
(
o
.
type
===
'
headline
'
||
o
.
is_me_message
)
{
part
.
setValue
(
''
);
return
;
const
whitelist_opts
=
{
'
whiteList
'
:
{
'
svg
'
:
[
'
xmlns
'
,
'
xmlns:xlink
'
,
'
class
'
,
'
width
'
,
'
height
'
],
'
image
'
:
[
'
width
'
,
'
height
'
,
'
preserveAspectRatio
'
,
'
xlink:href
'
]
}
};
const
tpl_svg
=
(
o
)
=>
xss
.
filterXSS
(
`<image width="
${
o
.
width
}
" height="
${
o
.
height
}
" preserveAspectRatio="xMidYMid meet" xlink:href="
${
o
.
image
}
"/>`
,
whitelist_opts
);
const
tpl_avatar
=
(
o
)
=>
html
`
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="avatar
${
o
.
classes
}
" width="
${
o
.
width
}
" height="
${
o
.
height
}
">
${
unsafeSVG
(
tpl_svg
(
o
))
}
</svg>
`
;
if
(
o
.
model
.
vcard
)
{
const
data
=
{
'
classes
'
:
'
avatar chat-msg__avatar
'
,
'
width
'
:
36
,
'
height
'
:
36
,
}
const
image_type
=
o
.
model
.
vcard
.
get
(
'
image_type
'
);
const
image
=
o
.
model
.
vcard
.
get
(
'
image
'
);
data
[
'
image
'
]
=
"
data:
"
+
image_type
+
"
;base64,
"
+
image
;
const
avatar
=
tpl_avatar
(
data
);
const
opts
=
{
'
whiteList
'
:
{
'
svg
'
:
[
'
xmlns
'
,
'
xmlns:xlink
'
,
'
class
'
,
'
width
'
,
'
height
'
],
'
image
'
:
[
'
width
'
,
'
height
'
,
'
preserveAspectRatio
'
,
'
xlink:href
'
]
}
};
part
.
setValue
(
html
`
${
unsafeHTML
(
xss
.
filterXSS
(
avatar
,
opts
))}
`
);
export
const
renderAvatar
=
directive
(
o
=>
part
=>
{
const
data
=
{
'
classes
'
:
o
.
classes
||
''
,
'
height
'
:
o
.
width
||
36
,
'
image
'
:
o
.
image
,
'
width
'
:
o
.
height
||
36
,
}
part
.
setValue
(
tpl_avatar
(
data
));
});
src/templates/profile_modal.js
View file @
09a79d60
import
{
html
}
from
"
lit-html
"
;
import
{
__
}
from
'
@converse/headless/i18n
'
;
import
avatar
from
"
./avatar.js
"
;
import
"
../components/image_picker.js
"
;
import
spinner
from
"
./spinner.js
"
;
import
{
modal_close_button
,
modal_header_close_button
}
from
"
./buttons
"
import
{
__
}
from
'
@converse/headless/i18n
'
;
import
{
_converse
,
converse
}
from
"
@converse/headless/converse-core
"
;
import
{
html
}
from
"
lit-html
"
;
import
{
modal_header_close_button
}
from
"
./buttons
"
;
const
u
=
converse
.
env
.
utils
;
const
alt_avatar
=
__
(
'
Your avatar image
'
);
const
heading_profile
=
__
(
'
Your Profile
'
);
const
i18n_fingerprint_checkbox_label
=
__
(
'
Checkbox for selecting the following fingerprint
'
);
const
i18n_device_without_fingerprint
=
__
(
'
Device without a fingerprint
'
);
...
...
@@ -38,7 +39,7 @@ const navigation = html`
const
fingerprint
=
(
o
)
=>
html
`
<span class="fingerprint">
${
o
.
utils
.
formatFingerprint
(
o
.
view
.
current_device
.
get
(
'
bundle
'
).
fingerprint
)}
</span>`
;
<span class="fingerprint">
${
u
.
formatFingerprint
(
o
.
view
.
current_device
.
get
(
'
bundle
'
).
fingerprint
)}
</span>`
;
const
device_with_fingerprint
=
(
o
)
=>
html
`
...
...
@@ -46,7 +47,7 @@ const device_with_fingerprint = (o) => html`
<label>
<input type="checkbox" value="
${
o
.
device
.
get
(
'
id
'
)}
"
aria-label="
${
i18n_fingerprint_checkbox_label
}
"/>
<span class="fingerprint">
${
o
.
utils
.
formatFingerprint
(
o
.
device
.
get
(
'
bundle
'
).
fingerprint
)}
</span>
<span class="fingerprint">
${
u
.
formatFingerprint
(
o
.
device
.
get
(
'
bundle
'
).
fingerprint
)}
</span>
</label>
</li>
`
;
...
...
@@ -108,16 +109,13 @@ export default (o) => html`
</div>
<div class="modal-body">
<span class="modal-alert"></span>
${
o
.
_converse
.
pluggable
.
plugins
[
'
converse-omemo
'
].
enabled
(
o
.
_converse
)
&&
navigation
}
${
_converse
.
pluggable
.
plugins
[
'
converse-omemo
'
].
enabled
(
_converse
)
&&
navigation
}
<div class="tab-content">
<div class="tab-pane active" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab">
<form class="converse-form converse-form--modal profile-form" action="#">
<div class="row">
<div class="col-auto">
<a class="change-avatar" href="#">
${
o
.
image
?
avatar
(
Object
.
assign
({
'
alt_text
'
:
alt_avatar
},
o
))
:
'
<canvas class="avatar" height="100px" width="100px"></canvas>
'
}
</a>
<input class="hidden" name="image" type="file"/>
<converse-image-picker image="
${
o
.
image
}
" width="
${
o
.
width
}
" height="
${
o
.
height
}
"></converse-image-picker>
</div>
<div class="col">
<div class="form-group">
...
...
@@ -153,10 +151,9 @@ export default (o) => html`
</div>
</form>
</div>
${
o
.
_converse
.
pluggable
.
plugins
[
'
converse-omemo
'
].
enabled
(
o
.
_converse
)
&&
omemo_page
(
o
)
}
${
_converse
.
pluggable
.
plugins
[
'
converse-omemo
'
].
enabled
(
_converse
)
&&
omemo_page
(
o
)
}
</div>
</div>
<div class="modal-footer">
${
modal_close_button
}
</div>
</div>
</div>
`
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment