Add frio-specific /compose module

This commit is contained in:
Hypolite Petovan 2019-07-14 21:49:38 -04:00
parent 0579252362
commit c24ca57f21
5 changed files with 537 additions and 2 deletions
src
App
Module/Item
view
templates/item
theme/frio/css

View file

@ -94,6 +94,7 @@ class Router
$this->routeCollector->addRoute(['GET'], '/attach/{item:\d+}', Module\Attach::class);
$this->routeCollector->addRoute(['GET'], '/babel', Module\Debug\Babel::class);
$this->routeCollector->addRoute(['GET'], '/bookmarklet', Module\Bookmarklet::class);
$this->routeCollector->addRoute(['GET', 'POST'], '/compose[/{type}]', Module\Item\Compose::class);
$this->routeCollector->addGroup('/contact', function (RouteCollector $collector) {
$collector->addRoute(['GET'], '[/]', Module\Contact::class);
$collector->addRoute(['GET', 'POST'], '/{id:\d+}[/]', Module\Contact::class);

176
src/Module/Item/Compose.php Normal file
View file

@ -0,0 +1,176 @@
<?php
namespace Friendica\Module\Item;
use Friendica\BaseModule;
use Friendica\Content\Feature;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\Model\Contact;
use Friendica\Model\FileTag;
use Friendica\Model\Group;
use Friendica\Model\Item;
use Friendica\Model\User;
use Friendica\Module\Login;
use Friendica\Network\HTTPException\NotImplementedException;
use Friendica\Util\Crypto;
class Compose extends BaseModule
{
public static function post()
{
if (!empty($_REQUEST['body'])) {
$_REQUEST['return'] = 'network';
require_once 'mod/item.php';
item_post(self::getApp());
} else {
notice(L10n::t('Please enter a post body.'));
}
}
public static function content()
{
if (!local_user()) {
return Login::form('compose', false);
}
$a = self::getApp();
if ($a->getCurrentTheme() !== 'frio') {
throw new NotImplementedException(L10n::t('This feature is only available with the frio theme.'));
}
/// @TODO Retrieve parameter from router
$posttype = $a->argv[1] ?? Item::PT_ARTICLE;
if (!in_array($posttype, [Item::PT_ARTICLE, Item::PT_PERSONAL_NOTE])) {
switch ($posttype) {
case 'note': $posttype = Item::PT_PERSONAL_NOTE; break;
default: $posttype = Item::PT_ARTICLE; break;
}
}
$user = User::getById(local_user(), ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'default-location']);
switch ($posttype) {
case Item::PT_PERSONAL_NOTE:
$compose_title = L10n::t('Compose new personal note');
$type = 'note';
$contact_allow = $a->contact['id'];
$group_allow = '';
break;
default:
$compose_title = L10n::t('Compose new post');
$type = 'post';
$contact_allow = implode(',', expand_acl($user['allow_cid']));
$group_allow = implode(',', expand_acl($user['allow_gid'])) ?: Group::FOLLOWERS;
break;
}
$title = $_REQUEST['title'] ?? '';
$category = $_REQUEST['category'] ?? '';
$body = $_REQUEST['body'] ?? '';
$location = $_REQUEST['location'] ?? $user['default-location'];
$wall = $_REQUEST['wall'] ?? $type == 'post';
$contact_allow = $_REQUEST['contact_allow'] ?? $contact_allow;
$group_allow = $_REQUEST['group_allow'] ?? $group_allow;
$contact_deny = $_REQUEST['contact_deny'] ?? implode(',', expand_acl($user['deny_cid']));
$group_deny = $_REQUEST['group_deny'] ?? implode(',', expand_acl($user['deny_gid']));
$visibility = ($contact_allow . $user['allow_gid'] . $user['deny_cid'] . $user['deny_gid']) ? 'custom' : 'public';
$acl_contacts = Contact::select(['id', 'name', 'addr', 'micro'], ['uid' => local_user(), 'pending' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]);
array_walk($acl_contacts, function (&$value) {
$value['type'] = 'contact';
});
$acl_groups = [
[
'id' => Group::FOLLOWERS,
'name' => L10n::t('Followers'),
'addr' => '',
'micro' => 'images/twopeople.png',
'type' => 'group',
],
[
'id' => Group::MUTUALS,
'name' => L10n::t('Mutuals'),
'addr' => '',
'micro' => 'images/twopeople.png',
'type' => 'group',
]
];
foreach (Group::getByUserId(local_user()) as $group) {
$acl_groups[] = [
'id' => $group['id'],
'name' => $group['name'],
'addr' => '',
'micro' => 'images/twopeople.png',
'type' => 'group',
];
}
$acl = array_merge($acl_groups, $acl_contacts);
$jotplugins = '';
Hook::callAll('jot_tool', $jotplugins);
// Output
$a->registerFooterScript('view/js/ajaxupload.js');
$a->registerFooterScript('view/js/linkPreview.js');
$a->registerFooterScript('view/asset/typeahead.js/dist/typeahead.bundle.js');
$a->registerFooterScript('view/theme/frio/frameworks/friendica-tagsinput/friendica-tagsinput.js');
$a->registerStylesheet('view/theme/frio/frameworks/friendica-tagsinput/friendica-tagsinput.css');
$a->registerStylesheet('view/theme/frio/frameworks/friendica-tagsinput/friendica-tagsinput-typeahead.css');
$tpl = Renderer::getMarkupTemplate('item/compose-footer.tpl');
$a->page['footer'] .= Renderer::replaceMacros($tpl, [
'$acl_contacts' => $acl_contacts,
'$acl_groups' => $acl_groups,
'$acl' => $acl,
]);
$tpl = Renderer::getMarkupTemplate('item/compose.tpl');
return Renderer::replaceMacros($tpl, [
'$compose_title'=> $compose_title,
'$id' => 0,
'$posttype' => $posttype,
'$type' => $type,
'$wall' => $wall,
'$default' => L10n::t(''),
'$mylink' => $a->removeBaseURL($a->contact['url']),
'$mytitle' => L10n::t('This is you'),
'$myphoto' => $a->removeBaseURL($a->contact['thumb']),
'$submit' => L10n::t('Submit'),
'$edbold' => L10n::t('Bold'),
'$editalic' => L10n::t('Italic'),
'$eduline' => L10n::t('Underline'),
'$edquote' => L10n::t('Quote'),
'$edcode' => L10n::t('Code'),
'$edimg' => L10n::t('Image'),
'$edurl' => L10n::t('Link'),
'$edattach' => L10n::t('Link or Media'),
'$prompttext' => L10n::t('Please enter a image/video/audio/webpage URL:'),
'$preview' => L10n::t('Preview'),
'$location_set' => L10n::t('Set your location'),
'$location_clear' => L10n::t('Clear the location'),
'$location_unavailable' => L10n::t('Location services are unavailable on your device'),
'$location_disabled' => L10n::t('Location services are disabled. Please check the website\'s permissions on your device'),
'$wait' => L10n::t('Please wait'),
'$placeholdertitle' => L10n::t('Set title'),
'$placeholdercategory' => (Feature::isEnabled(local_user(),'categories') ? L10n::t('Categories (comma-separated list)') : ''),
'$title' => $title,
'$category' => $category,
'$body' => $body,
'$location' => $location,
'$visibility' => $visibility,
'$contact_allow'=> $contact_allow,
'$group_allow' => $group_allow,
'$contact_deny' => $contact_deny,
'$group_deny' => $group_deny,
'$jotplugins' => $jotplugins,
'$sourceapp' => L10n::t($a->sourcename),
'$rand_num' => Crypto::randomDigits(12)
]);
}
}

View file

@ -0,0 +1,227 @@
<script type="text/javascript">
function updateLocationButtonDisplay(location_button, location_input)
{
location_button.classList.remove('btn-primary');
if (location_input.value) {
location_button.disabled = false;
location_button.classList.add('btn-primary');
location_button.title = location_button.dataset.titleClear;
} else if (!"geolocation" in navigator) {
location_button.disabled = true;
location_button.title = location_button.dataset.titleUnavailable;
} else if (location_button.disabled) {
location_button.title = location_button.dataset.titleDisabled;
} else {
location_button.title = location_button.dataset.titleSet;
}
}
$(function() {
// Jot attachment live preview.
let $textarea = $('#comment-edit-text-0');
$textarea.linkPreview();
$textarea.keyup(function(){
var textlen = $(this).val().length;
$('#character-counter').text(textlen);
});
$textarea.editor_autocomplete(baseurl+"/acl");
$textarea.bbco_autocomplete('bbcode');
let $acl_allow_input = $('#acl_allow');
let $group_allow_input = $('[name=group_allow]');
let $contact_allow_input = $('[name=contact_allow]');
let $acl_deny_input = $('#acl_deny');
let $group_deny_input = $('[name=group_deny]');
let $contact_deny_input = $('[name=contact_deny]');
// Visibility accordion
// Prevents open panel to collapse
// @see https://stackoverflow.com/a/43593116
$('[data-toggle="collapse"]').click(function(e) {
target = $(this).attr('href');
if ($(target).hasClass('in')) {
e.preventDefault(); // to stop the page jump to the anchor target.
e.stopPropagation()
}
});
$('#visibility-public-panel').on('show.bs.collapse', function() {
$('#visibility-public').prop('checked', true);
$group_allow_input.prop('disabled', true);
$contact_allow_input.prop('disabled', true);
$group_deny_input.prop('disabled', true);
$contact_deny_input.prop('disabled', true);
});
$('#visibility-custom-panel').on('show.bs.collapse', function() {
$('#visibility-custom').prop('checked', true);
$group_allow_input.prop('disabled', false);
$contact_allow_input.prop('disabled', false);
$group_deny_input.prop('disabled', false);
$contact_deny_input.prop('disabled', false);
});
if (document.querySelector('input[name="visibility"]:checked').value === 'custom') {
$('#visibility-custom-panel').collapse({parent: '#visibility-accordion'});
}
// Custom visibility tags inputs
let acl_groups = new Bloodhound({
local: {{$acl_groups|@json_encode nofilter}},
identify: function(obj) { return obj.id; },
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(['name']),
queryTokenizer: Bloodhound.tokenizers.whitespace,
});
let acl_contacts = new Bloodhound({
local: {{$acl_contacts|@json_encode nofilter}},
identify: function(obj) { return obj.id; },
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(['name', 'addr']),
queryTokenizer: Bloodhound.tokenizers.whitespace,
});
let acl = new Bloodhound({
local: {{$acl|@json_encode nofilter}},
identify: function(obj) { return obj.id; },
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(['name', 'addr']),
queryTokenizer: Bloodhound.tokenizers.whitespace,
});
acl.initialize();
let suggestionTemplate = function (item) {
return '<div><img src="' + item.micro + '" alt="" style="float: left; width: auto; height: 2.8em; margin-right: 0.5em;"> <strong>' + item.name + '</strong><br /><em>' + item.addr + '</em></div>';
};
$acl_allow_input.tagsinput({
confirmKeys: [13, 44],
cancelConfirmKeysOnEmpty: true,
freeInput: false,
tagClass: function(item) {
switch (item.type) {
case 'group' : return 'label label-primary';
case 'contact' :
default:
return 'label label-info';
}
},
itemValue: 'id',
itemText: 'name',
itemThumb: 'micro',
itemTitle: function(item) {
return item.addr;
},
typeaheadjs: {
name: 'contacts',
displayKey: 'name',
templates: {
suggestion: suggestionTemplate
},
source: acl.ttAdapter()
}
});
$acl_deny_input
.tagsinput({
confirmKeys: [13, 44],
freeInput: false,
tagClass: function(item) {
switch (item.type) {
case 'group' : return 'label label-primary';
case 'contact' :
default:
return 'label label-info';
}
},
itemValue: 'id',
itemText: 'name',
itemThumb: 'micro',
itemTitle: function(item) {
return item.addr;
},
typeaheadjs: {
name: 'contacts',
displayKey: 'name',
templates: {
suggestion: suggestionTemplate
},
source: acl.ttAdapter()
}
});
// Import existing ACL into the tags input fields.
$group_allow_input.val().split(',').forEach(function (val) {
$acl_allow_input.tagsinput('add', acl_groups.get(val)[0]);
});
$contact_allow_input.val().split(',').forEach(function (val) {
$acl_allow_input.tagsinput('add', acl_contacts.get(val)[0]);
});
$group_deny_input.val().split(',').forEach(function (val) {
$acl_deny_input.tagsinput('add', acl_groups.get(val)[0]);
});
$contact_deny_input.val().split(',').forEach(function (val) {
$acl_deny_input.tagsinput('add', acl_contacts.get(val)[0]);
});
// Anti-duplicate callback + acl fields value generation
$acl_allow_input.on('itemAdded', function (event) {
// Removes duplicate in the opposite acl box
$acl_deny_input.tagsinput('remove', event.item);
// Update the real acl field
$group_allow_input.val('');
$contact_allow_input.val('');
[].forEach.call($acl_allow_input.tagsinput('items'), function (item) {
if (item.type === 'group') {
$group_allow_input.val($group_allow_input.val() + '<' + item.id + '>');
} else {
$contact_allow_input.val($contact_allow_input.val() + '<' + item.id + '>');
}
});
});
$acl_deny_input.on('itemAdded', function (event) {
// Removes duplicate in the opposite acl box
$acl_allow_input.tagsinput('remove', event.item);
// Update the real acl field
$group_deny_input.val('');
$contact_deny_input.val('');
[].forEach.call($acl_deny_input.tagsinput('items'), function (item) {
if (item.type === 'group') {
$group_deny_input.val($group_allow_input.val() + '<' + item.id + '>');
} else {
$contact_deny_input.val($contact_allow_input.val() + '<' + item.id + '>');
}
});
});
let location_button = document.getElementById('profile-location');
let location_input = document.getElementById('jot-location');
updateLocationButtonDisplay(location_button, location_input);
location_input.addEventListener('change', function () {
updateLocationButtonDisplay(location_button, location_input);
});
location_input.addEventListener('keyup', function () {
updateLocationButtonDisplay(location_button, location_input);
});
location_button.addEventListener('click', function() {
if (location_input.value) {
location_input.value = '';
updateLocationButtonDisplay(location_button, location_input);
} else if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(function(position) {
location_input.value = position.coords.latitude + ', ' + position.coords.longitude;
updateLocationButtonDisplay(location_button, location_input);
}, function (error) {
location_button.disabled = true;
updateLocationButtonDisplay(location_button, location_input);
});
}
});
})
</script>

View file

@ -0,0 +1,129 @@
<div class="generic-page-wrapper">
<h2>{{$compose_title}}</h2>
<div id="profile-jot-wrapper">
<form class="comment-edit-form" data-item-id="{{$id}}" id="comment-edit-form-{{$id}}" action="compose/{{$type}}" method="post">
{{*<!--<input type="hidden" name="return" value="{{$return_path}}" />-->*}}
<input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" />
<input type="hidden" name="post_id_random" value="{{$rand_num}}" />
<input type="hidden" name="post_type" value="{{$posttype}}" />
<input type="hidden" name="wall" value="{{$wall}}" />
<div id="jot-title-wrap">
<input type="text" name="title" id="jot-title" class="jothidden jotforms form-control" placeholder="{{$placeholdertitle}}" title="{{$placeholdertitle}}" value="{{$title}}" tabindex="1"/>
</div>
{{if $placeholdercategory}}
<div id="jot-category-wrap">
<input name="category" id="jot-category" class="jothidden jotforms form-control" type="text" placeholder="{{$placeholdercategory}}" title="{{$placeholdercategory}}" value="{{$category}}" tabindex="2"/>
</div>
{{/if}}
<p class="comment-edit-bb-{{$id}} comment-icon-list">
<span>
<button type="button" class="btn btn-sm icon bb-img" aria-label="{{$edimg}}" title="{{$edimg}}" data-role="insert-formatting" data-bbcode="img" data-id="{{$id}}" tabindex="7">
<i class="fa fa-picture-o"></i>
</button>
<button type="button" class="btn btn-sm icon bb-attach" aria-label="{{$edattach}}" title="{{$edattach}}" ondragenter="return commentLinkDrop(event, {{$id}});" ondragover="return commentLinkDrop(event, {{$id}});" ondrop="commentLinkDropper(event);" onclick="commentGetLink({{$id}}, '{{$prompttext}}');" tabindex="8">
<i class="fa fa-paperclip"></i>
</button>
</span>
<span>
<button type="button" class="btn btn-sm icon bb-url" aria-label="{{$edurl}}" title="{{$edurl}}" onclick="insertFormatting('url',{{$id}});" tabindex="9">
<i class="fa fa-link"></i>
</button>
<button type="button" class="btn btn-sm icon underline" aria-label="{{$eduline}}" title="{{$eduline}}" onclick="insertFormatting('u',{{$id}});" tabindex="10">
<i class="fa fa-underline"></i>
</button>
<button type="button" class="btn btn-sm icon italic" aria-label="{{$editalic}}" title="{{$editalic}}" onclick="insertFormatting('i',{{$id}});" tabindex="11">
<i class="fa fa-italic"></i>
</button>
<button type="button" class="btn btn-sm icon bold" aria-label="{{$edbold}}" title="{{$edbold}}" onclick="insertFormatting('b',{{$id}});" tabindex="12">
<i class="fa fa-bold"></i>
</button>
<button type="button" class="btn btn-sm icon quote" aria-label="{{$edquote}}" title="{{$edquote}}" onclick="insertFormatting('quote',{{$id}});" tabindex="13">
<i class="fa fa-quote-left"></i>
</button>
</span>
</p>
<p>
<textarea id="comment-edit-text-{{$id}}" class="comment-edit-text form-control text-autosize" name="body" placeholder="{{$default}}" rows="7" tabindex="3">{{$body}}</textarea>
</p>
<p class="comment-edit-submit-wrapper">
{{if $type == 'post'}}
<span role="presentation" class="form-inline">
<input type="text" name="location" class="form-control" id="jot-location" value="{{$location}}" placeholder="{{$location_set}}"/>
<button type="button" class="btn btn-sm icon" id="profile-location"
data-title-set="{{$location_set}}"
data-title-disabled="{{$location_disabled}}"
data-title-unavailable="{{$location_unavailable}}"
data-title-clear="{{$location_clear}}"
title="{{$location_set}}"
tabindex="6">
<i class="fa fa-map-marker" aria-hidden="true"></i>
</button>
</span>
{{/if}}
<span role="presentation" id="profile-rotator-wrapper">
<img role="presentation" id="profile-rotator" src="images/rotator.gif" alt="{{$wait}}" title="{{$wait}}" style="display: none;" />
</span>
<span role="presentation" id="character-counter" class="grey text-info"></span>
{{if $preview}}
<button type="button" class="btn btn-defaul btn-sm" onclick="preview_comment({{$id}});" id="comment-edit-preview-link-{{$id}}" tabindex="5"><i class="fa fa-eye"></i> {{$preview}}</button>
{{/if}}
<button type="submit" class="btn btn-primary btn-sm" id="comment-edit-submit-{{$id}}" name="submit" tabindex="4"><i class="fa fa-envelope"></i> {{$submit}}</button>
</p>
<div id="comment-edit-preview-{{$id}}" class="comment-edit-preview" style="display:none;"></div>
<input type="hidden" name="group_allow" value="{{$group_allow}}" {{if $visibility == 'public'}}disabled{{/if}}/>
<input type="hidden" name="contact_allow" value="{{$contact_allow}}" {{if $visibility == 'public'}}disabled{{/if}}/>
<input type="hidden" name="group_deny" value="{{$group_deny}}" {{if $visibility == 'public'}}disabled{{/if}}/>
<input type="hidden" name="contact_deny" value="{{$contact_deny}}" {{if $visibility == 'public'}}disabled{{/if}}/>
{{if $type == 'post'}}
<h3>Visibility</h3>
<div class="panel-group" id="visibility-accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-success">
<div class="panel-heading" role="tab" id="visibility-public-heading" class="" role="button" data-toggle="collapse" data-parent="#visibility-accordion" href="#visibility-public-panel" aria-expanded="true" aria-controls="visibility-public-panel">
<label>
<input type="radio" name="visibility" id="visibility-public" value="public" {{if $visibility == 'public'}}checked{{/if}} style="display:none">
<i class="fa fa-globe"></i> Public
</label>
</div>
<div id="visibility-public-panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="visibility-public-heading">
<div class="panel-body">
<p>This post will be sent to all your followers and can be seen in the community pages and by anyone with its link.</p>
</div>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading" role="tab" id="visibility-custom-heading" class="collapsed" role="button" data-toggle="collapse" data-parent="#visibility-accordion" href="#visibility-custom-panel" aria-expanded="true" aria-controls="visibility-custom-panel">
<label>
<input type="radio" name="visibility" id="visibility-custom" value="custom" {{if $visibility == 'custom'}}checked{{/if}} style="display:none">
<i class="fa fa-lock"></i> Custom
</label>
</div>
<div id="visibility-custom-panel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="visibility-custom-heading">
<div class="panel-body">
<p>This post will be sent only to the people in the first box, to the exception of the people mentioned in the second box.
It won't be visible in the community pages nor with its link.</p>
<div class="form-group">
<label for="acl_allow">Deliver to:</label>
<input type="text" class="form-control input-lg" id="acl_allow">
</div>
<div class="form-group">
<label for="acl_deny">Except to:</label>
<input type="text" class="form-control input-lg" id="acl_deny">
</div>
</div>
</div>
</div>
</div>
<div class="jotplugins">
{{$jotplugins nofilter}}
</div>
{{/if}}
</form>
</div>
</div>

View file

@ -1371,7 +1371,8 @@ section #jotOpen {
#jot-text-wrap .preview textarea {
width: 100%;
}
#preview_profile-jot-text {
#preview_profile-jot-text,
.comment-edit-form .preview {
position: relative;
padding: 0px 10px;
margin-top: -2px;
@ -1382,7 +1383,8 @@ section #jotOpen {
background: #fff;
color: #555;
}
textarea#profile-jot-text:focus + #preview_profile-jot-text {
textarea#profile-jot-text:focus + #preview_profile-jot-text,
textarea.comment-edit-text:focus + .comment-edit-form .preview {
border: 2px solid #6fdbe8;
border-top: none;
}