Browse Source

Add contact banners support in Movim

pull/1111/head
Timothée Jaussoin 3 years ago
parent
commit
a964c1ec53
  1. 1
      CHANGELOG.md
  2. 7
      app/Contact.php
  3. 7
      app/helpers/UtilsHelper.php
  4. 41
      app/widgets/Avatar/Avatar.php
  5. 29
      app/widgets/Avatar/_avatar.tpl
  6. 36
      app/widgets/Avatar/_avatar_banner_form.tpl
  7. 12
      app/widgets/Avatar/locales.ini
  8. 2
      app/widgets/ContactActions/_contactactions_drawer.tpl
  9. 23
      app/widgets/ContactHeader/_contactheader.tpl
  10. 6
      lib/moxl/src/Stanza/Avatar.php
  11. 4
      lib/moxl/src/Utils.php
  12. 17
      lib/moxl/src/Xec/Action/Avatar/Set.php
  13. 28
      lib/moxl/src/Xec/Action/Pubsub/GetItem.php
  14. 12
      lib/moxl/src/Xec/Action/Pubsub/GetItems.php
  15. 3
      lib/moxl/src/Xec/Handler.php
  16. 23
      lib/moxl/src/Xec/Payload/BannerMetadata.php
  17. 20
      public/scripts/movim_avatar.js
  18. 51
      public/scripts/movim_utils.js
  19. 7
      public/theme/css/header.css

1
CHANGELOG.md

@ -26,6 +26,7 @@ v0.21 (trunk)
* Replace support of XEP-0049 by XEP-0223 to save Movim configuration
* Handle vCard4 phone numbers
* Update the CSS to Material 3, make things a bit more roundy
* Add contact banners support in Movim
v0.20
---------------------------

7
app/Contact.php

@ -127,6 +127,13 @@ class Contact extends Model
return !empty($this->id) ? getPhoto($this->id, $size) : null;
}
public function getBanner($size = 'xxl')
{
$banner = !empty($this->id) ? getPhoto($this->id . '_banner', $size) : null;
return $banner == null ? $this->getPhoto($size) : $banner;
}
public function setLocation($item)
{
// Clear

7
app/helpers/UtilsHelper.php

@ -727,7 +727,14 @@ function requestURL(string $url, int $timeout = 10, $post = false, bool $json =
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
// Disable SSL if the host requested is the local one
if (parse_url(config('daemon.url'), PHP_URL_HOST) == parse_url($url, PHP_URL_HOST)) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
}
$content = curl_exec($ch);
return curl_errno($ch) == 0 ? $content : false;
}

41
app/widgets/Avatar/Avatar.php

@ -13,6 +13,7 @@ class Avatar extends \Movim\Widget\Base
$this->registerEvent('avatar_get_handle', 'onGetAvatar');
$this->registerEvent('avatar_set_handle', 'onSetAvatar');
$this->registerEvent('avatar_set_pubsub', 'onSetBanner');
$this->registerEvent('avatar_set_errorfeaturenotimplemented', 'onMyAvatarError');
$this->registerEvent('avatar_set_errorbadrequest', 'onMyAvatarError');
$this->registerEvent('avatar_set_errornotallowed', 'onMyAvatarError');
@ -21,6 +22,16 @@ class Avatar extends \Movim\Widget\Base
public function onSetAvatar($packet)
{
$this->ajaxGetAvatar();
$this->rpc('MovimTpl.fill', '#avatar', $this->prepareForm());
}
public function onSetBanner($packet)
{
global $loop;
$loop->addTimer(2, function () {
$this->rpc('MovimTpl.fill', '#avatar', $this->prepareForm());
});
}
public function onGetAvatar($packet)
@ -50,6 +61,13 @@ class Avatar extends \Movim\Widget\Base
Dialog::fill($view->draw('_avatar_form'));
}
public function ajaxGetBannerForm()
{
$view = $this->tpl();
$view->assign('me', \App\Contact::firstOrNew(['id' => $this->user->id]));
Dialog::fill($view->draw('_avatar_banner_form'));
}
public function ajaxGetAvatar()
{
$r = new Get;
@ -80,4 +98,27 @@ class Avatar extends \Movim\Widget\Base
$p->remove('jpeg');
}
public function ajaxBannerSubmit($banner)
{
if (empty($banner->photobin->value)) return;
$key = $this->user->id.'banner';
$p = new Image;
$p->fromBase($banner->photobin->value);
$p->setKey($key);
$p->save(false, false, 'jpeg', 60);
// Reload
$p->load('jpeg');
$r = new Set;
$r->setNode('urn:xmpp:movim-banner:0')
->setUrl(Image::getOrCreate($key, false, false, 'jpeg', true))
->setWidthMetadata(1280)
->setHeightMetadata(320)
->setData($p->toBase())
->request();
}
}

29
app/widgets/Avatar/_avatar.tpl

@ -1,5 +1,5 @@
<ul class="list thick">
<li class="active" onclick="Avatar_ajaxGetForm()">
<ul class="list thick flex">
<li class="block active" onclick="Avatar_ajaxGetForm()">
{$url = $me->getPhoto()}
{if="$url"}
<span
@ -24,4 +24,29 @@
{/if}
</div>
</li>
<li class="block active" onclick="Avatar_ajaxGetBannerForm()">
{$url = $me->getBanner()}
{if="$url"}
<span
class="primary icon bubble"
style="background-image: url({$url})">
</span>
{else}
<span
class="primary icon bubble color {$me->jid|stringToColor}">
<i class="material-icons">person</i>
</span>
{/if}
<span class="control icon gray">
<i class="material-icons">chevron_right</i>
</span>
<div>
<p>{$c->__('banner.change')}</p>
{if="$url"}
<p>{$c->__('avatar.upload_new')}</p>
{else}
<p>{$c->__('banner.missing')}</p>
{/if}
</div>
</li>
</ul>

36
app/widgets/Avatar/_avatar_banner_form.tpl

@ -0,0 +1,36 @@
<section class="scroll">
<form name="bannerform">
<img class="avatar" src="{$me->getBanner('o') ?? ''}">
<div class="placeholder">
<i class="material-icons">image</i>
<h1>{$c->__('banner.missing')}</h1>
</div>
<input type="hidden" name="photobin"/>
</form>
<ul class="list thick divided">
<li>
<span class="primary icon bubble color green">
<i class="material-icons">attach_file</i>
</span>
<div>
<p>{$c->__('avatar.file')}</p>
<p><input type="file" onchange="MovimAvatar.file(this.files, 'bannerform', 1280, 320);"></p>
</div>
</li>
</ul>
</section>
<div>
<button onclick="Dialog_ajaxClear()" class="button flat">
{$c->__('button.close')}
</button>
<button
type="button"
onclick="
Avatar_ajaxBannerSubmit(MovimUtils.formToJson('bannerform'));
this.value = '{$c->__('button.submitting')}';
this.className='button flat inactive';"
class="button flat"
>
{$c->__('button.submit')}
</button>
</div>

12
app/widgets/Avatar/locales.ini

@ -1,11 +1,13 @@
[avatar]
file = File
use_it = Use it
webcam = Webcam
cheese = Cheese!
snapshot = Take a webcam snapshot
updated = Avatar Updated
not_updated = Avatar Not Updated
missing = No avatar defined yet
change = Change my avatar
upload_new = Upload a new picture
upload_new = Upload a new picture
[banner]
change = Change my banner
updated = Banner Updated
not_updated = Banner Not Updated
missing = No banner defined yet

2
app/widgets/ContactActions/_contactactions_drawer.tpl

@ -2,7 +2,7 @@
{$url = $contact->getPhoto()}
<header class="big"
{if="$url"}
style="background-image: linear-gradient(to bottom, rgba(23,23,23,0.8) 0%, rgba(23,23,23,0.5) 100%), url('{$contact->getPhoto('xxl')}');"
style="background-image: linear-gradient(to bottom, rgba(23,23,23,0.8) 0%, rgba(23,23,23,0.5) 100%), url('{$contact->getBanner('xxl')}');"
{/if}
>
<ul class="list thick">

23
app/widgets/ContactHeader/_contactheader.tpl

@ -4,37 +4,45 @@
</a>
{/if}
{$banner = $contact->getBanner()}
<header class="big top"
style="background-image:
linear-gradient(to top, rgba(23,23,23,0.9) 0, rgba(23,23,23,0.6) 7rem, rgba(23,23,23,0) 12rem),
url('{$banner}');
">
<ul class="list thick">
<li>
<li class="block large">
{if="!$contact->isMe()"}
<span class="control icon active gray" onclick="ContactActions_ajaxChat('{$contact->id|echapJS}')"
<span class="control icon active white" onclick="ContactActions_ajaxChat('{$contact->id|echapJS}')"
title="{$c->__('button.chat')}">
<i class="material-icons">comment</i>
</span>
{/if}
{if="$roster"}
<span class="control icon active gray divided" onclick="ContactHeader_ajaxEditContact('{$contact->id|echapJS}')"
<span class="control icon active white divided" onclick="ContactHeader_ajaxEditContact('{$contact->id|echapJS}')"
title="{$c->__('button.edit')}">
<i class="material-icons">edit</i>
</span>
<span class="control icon active gray" onclick="ContactHeader_ajaxDeleteContact('{$contact->id|echapJS}')"
<span class="control icon active white" onclick="ContactHeader_ajaxDeleteContact('{$contact->id|echapJS}')"
title="{$c->__('button.delete')}">
<i class="material-icons">delete</i>
</span>
{else}
{if="$contact->isMe()"}
<span class="control icon active gray divided" onclick="MovimUtils.redirect('{$c->route('conf')}')"
<span class="control icon active white divided" onclick="MovimUtils.redirect('{$c->route('conf')}')"
title="{$c->__('button.edit')}">
<i class="material-icons">edit</i>
</span>
{else}
<span class="control icon active gray divided" onclick="ContactActions_ajaxAddAsk('{$contact->id|echapJS}')"
<span class="control icon active white divided" onclick="ContactActions_ajaxAddAsk('{$contact->id|echapJS}')"
title="{$c->__('button.add')}">
<i class="material-icons">person_add</i>
</span>
{/if}
{/if}
<span class="primary icon active gray" onclick="history.back()">
<span class="primary icon active white" onclick="history.back()">
<i class="material-icons">arrow_back</i>
</span>
{$url = $contact->getPhoto('m')}
@ -56,3 +64,4 @@
</div>
</li>
</ul>
{if="$banner"}</header>{/if}

6
lib/moxl/src/Stanza/Avatar.php

@ -39,7 +39,7 @@ class Avatar
\Moxl\API::request($xml);
}
public static function setMetadata($data, $url = false, $to = false, $node = false)
public static function setMetadata($data, $url = false, $to = false, $node = false, $width = 350, $height = 350)
{
$decoded = base64_decode($data);
@ -65,8 +65,8 @@ class Avatar
$info->setAttribute('url', $url);
}
$info->setAttribute('height', '410');
$info->setAttribute('width', '410');
$info->setAttribute('width', $width);
$info->setAttribute('height', $height);
$info->setAttribute('type', 'image/jpeg');
$info->setAttribute('id', sha1($decoded));
$info->setAttribute('bytes', strlen($decoded));

4
lib/moxl/src/Utils.php

@ -25,6 +25,10 @@ class Utils
'urn:xmpp:avatar:data',
'urn:xmpp:avatar:metadata',
'urn:xmpp:avatar:metadata+notify',
'urn:xmpp:movim-banner:0',
'urn:xmpp:movim-banner:0+notify',
'urn:xmpp:receipts',
'urn:xmpp:carbons:2',
'jabber:iq:version',

17
lib/moxl/src/Xec/Action/Avatar/Set.php

@ -11,6 +11,8 @@ class Set extends Action
protected $_to = false;
protected $_node = false;
protected $_url = false;
protected $_widthMetadata = 350;
protected $_heightMetadata = 350;
public function request()
{
@ -20,7 +22,20 @@ class Set extends Action
Avatar::set($this->_data, $this->_to, $this->_node);
}
Avatar::setMetadata($this->_data, $this->_url, $this->_to, $this->_node);
Avatar::setMetadata($this->_data, $this->_url, $this->_to, $this->_node,
$this->_widthMetadata, $this->_heightMetadata);
}
public function setWidthMetadata($width)
{
$this->_widthMetadata = $width;
return $this;
}
public function setHeightMetadata($height)
{
$this->_heightMetadata = $height;
return $this;
}
public function handle($stanza, $parent = false)

28
lib/moxl/src/Xec/Action/Pubsub/GetItem.php

@ -74,19 +74,21 @@ class GetItem extends Errors
if ($i && $i->avatarhash !== (string)$item->metadata->info->attributes()->id) {
$p = new Image;
$p->fromURL((string)$item->metadata->info->attributes()->url);
$p->setKey((string)$item->metadata->info->attributes()->id);
$p->save();
$i->avatarhash = (string)$item->metadata->info->attributes()->id;
$i->save();
$this->method('avatar');
$this->pack([
'server' => $this->_to,
'node' => $this->_node
]);
$this->deliver();
if ($p->fromURL((string)$item->metadata->info->attributes()->url)) {
$p->setKey((string)$item->metadata->info->attributes()->id);
$p->save();
$i->avatarhash = (string)$item->metadata->info->attributes()->id;
$i->save();
$this->method('avatar');
$this->pack([
'server' => $this->_to,
'node' => $this->_node
]);
$this->deliver();
}
}
}
}

12
lib/moxl/src/Xec/Action/Pubsub/GetItems.php

@ -81,12 +81,14 @@ class GetItems extends Errors
if ($i && $i->avatarhash !== (string)$item->metadata->info->attributes()->id) {
$p = new Image;
$p->fromURL((string)$item->metadata->info->attributes()->url);
$p->setKey((string)$item->metadata->info->attributes()->id);
$p->save();
$i->avatarhash = (string)$item->metadata->info->attributes()->id;
$i->save();
if ($p->fromURL((string)$item->metadata->info->attributes()->url)) {
$p->setKey((string)$item->metadata->info->attributes()->id);
$p->save();
$i->avatarhash = (string)$item->metadata->info->attributes()->id;
$i->save();
}
}
}
}

3
lib/moxl/src/Xec/Handler.php

@ -155,6 +155,9 @@ class Handler
'1cb493832467273efa384bbffa6dc35a' => 'Avatar',
'0f59aa7fb0492a008df1b807e91dda3b' => 'AvatarMetadata',
'64d80ef76ceb442578e658fa39cde8c9' => 'BannerMetadata', // Movim specific for now
'36fe2745bdc72b1682be2c008d547e3d' => 'Vcard4',
'0923dd6b12f46f658b4273104a129ec9' => 'JinglePropose',

23
lib/moxl/src/Xec/Payload/BannerMetadata.php

@ -0,0 +1,23 @@
<?php
namespace Moxl\Xec\Payload;
use Movim\Image;
class BannerMetadata extends Payload
{
public function handle($stanza, $parent = false)
{
$jid = baseJid((string)$parent->attributes()->from);
if (isset($stanza->items->item->metadata->info)
&& isset($stanza->items->item->metadata->info->attributes()->url)) {
$p = new Image;
if ($p->fromURL((string)$stanza->items->item->metadata->info->attributes()->url)) {
$p->setKey($jid . '_banner');
$p->save();
}
}
}
}

20
public/scripts/movim_avatar.js

@ -1,5 +1,5 @@
var MovimAvatar = {
file : function(files, formname) {
file : function(files, formname, width = 350, height = 350) {
var f = files[0];
if (!f.type.match(/image.*/)) {
console.log("Not a picture !");
@ -10,7 +10,7 @@ var MovimAvatar = {
reader.onload = function (ev) {
MovimUtils.getOrientation(f, function(orientation) {
MovimAvatar.preview(ev.target.result, orientation, formname);
MovimAvatar.preview(ev.target.result, orientation, formname, width, height);
});
};
};
@ -19,9 +19,10 @@ var MovimAvatar = {
document.querySelector('form[name=' + formname + '] img').src = '';
document.querySelector('form[name=' + formname + '] input[name="photobin"]').value = '';
},
preview : function(src, orientation, formname) {
preview : function(src, orientation, formname, setWidth, setHeight) {
var canvas = document.createElement('canvas');
width = height = canvas.width = canvas.height = 350;
width = canvas.width = setWidth;
height = canvas.height = setHeight;
var image = new Image();
image.src = src;
@ -39,16 +40,7 @@ var MovimAvatar = {
default: ctx.transform(1, 0, 0, 1, 0, 0);
}
if (image.width == image.height) {
ctx.drawImage(image, 0, 0, width, height);
} else {
minVal = parseInt(Math.min(image.width, image.height));
if (image.width > image.height) {
ctx.drawImage(image, (parseInt(image.width) - minVal) / 2, 0, minVal, minVal, 0, 0, width, height);
} else {
ctx.drawImage(image, 0, (parseInt(image.height) - minVal) / 2, minVal, minVal, 0, 0, width, height);
}
}
MovimUtils.drawImageProp(ctx, image);
var base64 = canvas.toDataURL('image/jpeg', 0.95);

51
public/scripts/movim_utils.js

@ -25,7 +25,7 @@ var MovimUtils = {
cleanupId: function (string) {
return 'id-' + string.replace(/([^a-z0-9]+)/gi, '-').toLowerCase();
},
hash (string) {
hash(string) {
var hash = 0;
if (string.length == 0) return hash;
@ -253,6 +253,53 @@ var MovimUtils = {
}
img.src = testImageURL
},
drawImageProp: function (ctx, img, x, y, w, h, offsetX, offsetY) {
if (arguments.length === 2) {
x = y = 0;
w = ctx.canvas.width;
h = ctx.canvas.height;
}
// default offset is center
offsetX = typeof offsetX === "number" ? offsetX : 0.5;
offsetY = typeof offsetY === "number" ? offsetY : 0.5;
// keep bounds [0.0, 1.0]
if (offsetX < 0) offsetX = 0;
if (offsetY < 0) offsetY = 0;
if (offsetX > 1) offsetX = 1;
if (offsetY > 1) offsetY = 1;
var iw = img.width,
ih = img.height,
r = Math.min(w / iw, h / ih),
nw = iw * r, // new prop. width
nh = ih * r, // new prop. height
cx, cy, cw, ch, ar = 1;
// decide which gap to fill
if (nw < w) ar = w / nw;
if (Math.abs(ar - 1) < 1e-14 && nh < h) ar = h / nh; // updated
nw *= ar;
nh *= ar;
// calc source rectangle
cw = iw / (nw / w);
ch = ih / (nh / h);
cx = (iw - cw) * offsetX;
cy = (ih - ch) * offsetY;
// make sure source rectangle is valid
if (cx < 0) cx = 0;
if (cy < 0) cy = 0;
if (cw > iw) cw = iw;
if (ch > ih) ch = ih;
// fill image in dest. rectangle
ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h);
},
arrayBufferToBase64: function (ab) {
return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
},
@ -285,7 +332,7 @@ var MovimUtils = {
range: function (start, end) {
return Array.from({ length: end - start + 1 }, (_, i) => i)
},
linkify: function(inputText) {
linkify: function (inputText) {
var replacedText, replacePattern1, replacePattern2, replacePattern3;
//URLs starting with http://, https://, or ftp://

7
public/theme/css/header.css

@ -9,8 +9,15 @@ header.big {
margin-bottom: 0.5rem;
}
header.big.top {
border-radius: 0 0 1rem 1rem;
padding-top: 15rem;
margin-bottom: 1rem;
}
header.big ul li {
padding-right: 0;
padding-left: 0;
}
/* Specific forms in header */

Loading…
Cancel
Save