Merge remote-tracking branch 'upstream/develop' into contact-id-new

This commit is contained in:
Michael 2019-08-02 16:52:34 +00:00
commit 9819d31591
33 changed files with 1735 additions and 190 deletions

View File

@ -32,12 +32,14 @@
"ezyang/htmlpurifier": "~4.7.0",
"friendica/json-ld": "^1.0",
"league/html-to-markdown": "~4.8.0",
"level-2/dice": ">1.0",
"lightopenid/lightopenid": "dev-master",
"michelf/php-markdown": "^1.7",
"mobiledetect/mobiledetectlib": "2.8.*",
"monolog/monolog": "^1.24",
"nikic/fast-route": "^1.3",
"paragonie/hidden-string": "^1.0",
"pear/console_table": "^1.3",
"pear/text_languagedetect": "1.*",
"pragmarx/google2fa": "^5.0",
"pragmarx/recovery": "^0.1.0",
@ -58,8 +60,7 @@
"npm-asset/fullcalendar": "^3.0.1",
"npm-asset/cropperjs": "1.2.2",
"npm-asset/imagesloaded": "4.1.4",
"pear/console_table": "^1.3",
"level-2/dice": ">1.0"
"npm-asset/typeahead.js": "^0.11.1"
},
"repositories": [
{

166
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9f101b2b4a651f425e155d0029223774",
"content-hash": "8c88c86ebce0bbf672356d65fadc0008",
"packages": [
{
"name": "asika/simple-console",
@ -1076,7 +1076,6 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.2.2.tgz",
"reference": null,
"shasum": "30dc7a7ce872155b23a33bd10ad4c76c0d613f55"
},
"require-dev": {
@ -1170,7 +1169,6 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz",
"reference": null,
"shasum": "8f18b0ce5c76a5d18017f71c0a795c65b9138f2a"
},
"type": "npm-asset-library",
@ -1213,7 +1211,6 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-3.10.0.tgz",
"reference": null,
"shasum": "cc5e87d518fd6550e142816a31dd191664847919"
},
"type": "npm-asset-library",
@ -1260,28 +1257,11 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/imagesloaded/-/imagesloaded-4.1.4.tgz",
"reference": null,
"shasum": "1376efcd162bb768c34c3727ac89cc04051f3cc7"
},
"require": {
"npm-asset/ev-emitter": ">=1.0.0,<2.0.0"
},
"require-dev": {
"npm-asset/chalk": ">=1.1.1,<2.0.0",
"npm-asset/cheerio": ">=0.19.0,<0.20.0",
"npm-asset/gulp": ">=3.9.0,<4.0.0",
"npm-asset/gulp-jshint": ">=1.11.2,<2.0.0",
"npm-asset/gulp-json-lint": ">=0.1.0,<0.2.0",
"npm-asset/gulp-rename": ">=1.2.2,<2.0.0",
"npm-asset/gulp-replace": ">=0.5.4,<0.6.0",
"npm-asset/gulp-requirejs-optimize": "dev-github:metafizzy/gulp-requirejs-optimize",
"npm-asset/gulp-uglify": ">=1.4.2,<2.0.0",
"npm-asset/gulp-util": ">=3.0.7,<4.0.0",
"npm-asset/highlight.js": ">=8.9.1,<9.0.0",
"npm-asset/marked": ">=0.3.5,<0.4.0",
"npm-asset/minimist": ">=1.2.0,<2.0.0",
"npm-asset/transfob": ">=1.0.0,<2.0.0"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1324,17 +1304,8 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.6.tgz",
"reference": null,
"shasum": "2736e332aaee73ccf0a14a5f0066391a0a13f4a3"
},
"require-dev": {
"npm-asset/grunt": "~0.4.2",
"npm-asset/grunt-contrib-cssmin": "~0.9.0",
"npm-asset/grunt-contrib-jshint": "~0.6.3",
"npm-asset/grunt-contrib-less": "~0.11.0",
"npm-asset/grunt-contrib-uglify": "~0.4.0",
"npm-asset/grunt-contrib-watch": "~0.6.1"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1365,35 +1336,8 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
"reference": null,
"shasum": "2c89d6889b5eac522a7eea32c14521559c6cbf02"
},
"require-dev": {
"npm-asset/commitplease": "2.0.0",
"npm-asset/core-js": "0.9.17",
"npm-asset/grunt": "0.4.5",
"npm-asset/grunt-babel": "5.0.1",
"npm-asset/grunt-cli": "0.1.13",
"npm-asset/grunt-compare-size": "0.4.0",
"npm-asset/grunt-contrib-jshint": "0.11.2",
"npm-asset/grunt-contrib-uglify": "0.9.2",
"npm-asset/grunt-contrib-watch": "0.6.1",
"npm-asset/grunt-git-authors": "2.0.1",
"npm-asset/grunt-jscs": "2.1.0",
"npm-asset/grunt-jsonlint": "1.0.4",
"npm-asset/grunt-npmcopy": "0.1.0",
"npm-asset/gzip-js": "0.3.2",
"npm-asset/jsdom": "5.6.1",
"npm-asset/load-grunt-tasks": "1.0.0",
"npm-asset/qunit-assert-step": "1.0.3",
"npm-asset/qunitjs": "1.17.1",
"npm-asset/requirejs": "2.1.17",
"npm-asset/sinon": "1.10.3",
"npm-asset/sizzle": "2.2.1",
"npm-asset/strip-json-comments": "1.0.3",
"npm-asset/testswarm": "1.1.0",
"npm-asset/win-spawn": "2.0.0"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1436,7 +1380,6 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/jquery-colorbox/-/jquery-colorbox-1.6.4.tgz",
"reference": null,
"shasum": "799452523a6c494839224ef702e807deb9c06cc5"
},
"require": {
@ -1483,7 +1426,6 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/jquery-datetimepicker/-/jquery-datetimepicker-2.5.20.tgz",
"reference": null,
"shasum": "687d6204b90b03dc93f725f8df036e1d061f37ac"
},
"require": {
@ -1541,15 +1483,8 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz",
"reference": null,
"shasum": "06f0335f16e353a695e7206bf50503cb523a6ee5"
},
"require-dev": {
"npm-asset/grunt": "~0.4.1",
"npm-asset/grunt-contrib-connect": "~0.5.0",
"npm-asset/grunt-contrib-jshint": "~0.7.1",
"npm-asset/grunt-contrib-uglify": "~0.2.7"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1596,7 +1531,6 @@
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"reference": null,
"shasum": "3c257f9839fc0e93ff53149632239eb90783ff66"
},
"type": "npm-asset-library",
@ -1707,6 +1641,58 @@
"homepage": "https://github.com/kartik-v/php-date-formatter",
"time": "2018-07-13T06:56:46+00:00"
},
{
"name": "npm-asset/typeahead.js",
"version": "0.11.1",
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/typeahead.js/-/typeahead.js-0.11.1.tgz",
"shasum": "4e64e671b22310a8606f4aec805924ba84b015b8"
},
"require": {
"npm-asset/jquery": ">=1.7"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
"url": "https://github.com/twitter/typeahead.js/issues"
},
"npm-asset-main": "dist/typeahead.bundle.js",
"npm-asset-directories": [],
"npm-asset-repository": {
"type": "git",
"url": "https://github.com/twitter/typeahead.js.git"
},
"npm-asset-scripts": {
"test": "./node_modules/karma/bin/karma start --single-run --browsers PhantomJS"
}
},
"authors": [
{
"name": "Twitter, Inc.",
"url": "https://twitter.com/twitteross"
},
{
"name": "Jake Harding",
"url": "https://twitter.com/JakeHarding"
},
{
"name": "Tim Trueman",
"url": "https://twitter.com/timtrueman"
},
{
"name": "Veljko Skarich",
"url": "https://twitter.com/vskarich"
}
],
"description": "fast and fully-featured autocomplete library",
"homepage": "http://twitter.github.com/typeahead.js",
"keywords": [
"autocomplete",
"typeahead"
],
"time": "2015-04-27T04:03:42+00:00"
},
{
"name": "paragonie/certainty",
"version": "v1.0.4",
@ -2883,7 +2869,7 @@
"time": "2017-03-25T17:14:26+00:00"
},
{
"name": "mikey179/vfsStream",
"name": "mikey179/vfsstream",
"version": "v1.6.5",
"source": {
"type": "git",
@ -2920,8 +2906,8 @@
"authors": [
{
"name": "Frank Kleine",
"homepage": "http://frankkleine.de/",
"role": "Developer"
"role": "Developer",
"homepage": "http://frankkleine.de/"
}
],
"description": "Virtual file system to mock the real file system in unit tests.",
@ -3353,8 +3339,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
"role": "lead",
"email": "sb@sebastian-bergmann.de"
}
],
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
@ -3621,8 +3607,8 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
"role": "lead",
"email": "sebastian@phpunit.de"
}
],
"description": "The PHP Unit Testing framework.",
@ -3795,7 +3781,7 @@
}
],
"description": "Provides the functionality to compare PHP values for equality",
"homepage": "https://github.com/sebastianbergmann/comparator",
"homepage": "http://www.github.com/sebastianbergmann/comparator",
"keywords": [
"comparator",
"compare",
@ -3897,7 +3883,7 @@
}
],
"description": "Provides functionality to handle HHVM/PHP environments",
"homepage": "https://github.com/sebastianbergmann/environment",
"homepage": "http://www.github.com/sebastianbergmann/environment",
"keywords": [
"Xdebug",
"environment",
@ -3965,7 +3951,7 @@
}
],
"description": "Provides the functionality to export PHP variables for visualization",
"homepage": "https://github.com/sebastianbergmann/exporter",
"homepage": "http://www.github.com/sebastianbergmann/exporter",
"keywords": [
"export",
"exporter"
@ -4017,7 +4003,7 @@
}
],
"description": "Snapshotting of global state",
"homepage": "https://github.com/sebastianbergmann/global-state",
"homepage": "http://www.github.com/sebastianbergmann/global-state",
"keywords": [
"global state"
],
@ -4119,7 +4105,7 @@
}
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"time": "2016-11-19T07:33:16+00:00"
},
{
@ -4209,16 +4195,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.11.0",
"version": "v1.9.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "82ebae02209c21113908c229e9883c419720738a"
"reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a",
"reference": "82ebae02209c21113908c229e9883c419720738a",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
"reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
"shasum": ""
},
"require": {
@ -4230,7 +4216,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
"dev-master": "1.9-dev"
}
},
"autoload": {
@ -4263,20 +4249,20 @@
"polyfill",
"portable"
],
"time": "2019-02-06T07:57:58+00:00"
"time": "2018-08-06T14:22:27+00:00"
},
{
"name": "symfony/yaml",
"version": "v3.4.30",
"version": "v3.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "051d045c684148060ebfc9affb7e3f5e0899d40b"
"reference": "61973ecda60e9f3561e929e19c07d4878b960fc1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/051d045c684148060ebfc9affb7e3f5e0899d40b",
"reference": "051d045c684148060ebfc9affb7e3f5e0899d40b",
"url": "https://api.github.com/repos/symfony/yaml/zipball/61973ecda60e9f3561e929e19c07d4878b960fc1",
"reference": "61973ecda60e9f3561e929e19c07d4878b960fc1",
"shasum": ""
},
"require": {
@ -4322,7 +4308,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2019-07-24T13:01:31+00:00"
"time": "2018-09-24T08:15:45+00:00"
},
{
"name": "webmozart/assert",

View File

@ -4,11 +4,11 @@
*/
use Friendica\App;
use Friendica\Content\Smilies;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Protocol;
use Friendica\Model\Contact;
use Friendica\Model\FileTag;
use Friendica\Model\Group;
use Friendica\Util\Strings;
/**
@ -20,18 +20,9 @@ use Friendica\Util\Strings;
function expand_acl($s) {
// turn string array of angle-bracketed elements into numeric array
// e.g. "<1><2><3>" => array(1,2,3);
$ret = [];
preg_match_all('/<(' . Group::FOLLOWERS . '|'. Group::MUTUALS . '|[0-9]+)>/', $s, $matches, PREG_PATTERN_ORDER);
if (strlen($s)) {
$t = str_replace('<', '', $s);
$a = explode('>', $t);
foreach ($a as $aa) {
if (intval($aa)) {
$ret[] = intval($aa);
}
}
}
return $ret;
return $matches[1];
}
@ -42,6 +33,8 @@ function expand_acl($s) {
function sanitise_acl(&$item) {
if (intval($item)) {
$item = '<' . intval(Strings::escapeTags(trim($item))) . '>';
} elseif (in_array($item, [Group::FOLLOWERS, Group::MUTUALS])) {
$item = '<' . $item . '>';
} else {
unset($item);
}

View File

@ -6,6 +6,7 @@ use Friendica\App;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Database\DBA;
use Friendica\Model\Group;
use Friendica\Model\Item;
function lockview_content(App $a)
@ -67,6 +68,19 @@ function lockview_content(App $a)
$l = [];
if (count($allowed_groups)) {
$key = array_search(Group::FOLLOWERS, $allowed_groups);
if ($key !== false) {
$l[] = '<b>' . L10n::t('Followers') . '</b>';
unset($allowed_groups[$key]);
}
$key = array_search(Group::MUTUALS, $allowed_groups);
if ($key !== false) {
$l[] = '<b>' . L10n::t('Mutuals') . '</b>';
unset($allowed_groups[$key]);
}
$r = q("SELECT `name` FROM `group` WHERE `id` IN ( %s )",
DBA::escape(implode(', ', $allowed_groups))
);
@ -89,6 +103,18 @@ function lockview_content(App $a)
}
if (count($deny_groups)) {
$key = array_search(Group::FOLLOWERS, $deny_groups);
if ($key !== false) {
$l[] = '<b><strike>' . L10n::t('Followers') . '</strike></b>';
unset($deny_groups[$key]);
}
$key = array_search(Group::MUTUALS, $deny_groups);
if ($key !== false) {
$l[] = '<b><strike>' . L10n::t('Mutuals') . '</strike></b>';
unset($deny_groups[$key]);
}
$r = q("SELECT `name` FROM `group` WHERE `id` IN ( %s )",
DBA::escape(implode(', ', $deny_groups))
);

View File

@ -643,7 +643,7 @@ function networkThreadedView(App $a, $update, $parent)
// NOTREACHED
}
$contacts = Group::expand([$gid]);
$contacts = Group::expand(local_user(), [$gid]);
if ((is_array($contacts)) && count($contacts)) {
$contact_str_self = '';

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);

View File

@ -25,6 +25,8 @@ class Protocol
const FEDERATED = [self::DFRN, self::DIASPORA, self::OSTATUS, self::ACTIVITYPUB];
const SUPPORT_PRIVATE = [self::DFRN, self::DIASPORA, self::MAIL, self::ACTIVITYPUB, self::PUMPIO];
// Supported through a connector
const DIASPORA2 = 'dspc'; // Diaspora connector
const LINKEDIN = 'lnkd'; // LinkedIn

View File

@ -2,12 +2,14 @@
/**
* @file src/Model/Group.php
*/
namespace Friendica\Model;
use Friendica\BaseModule;
use Friendica\BaseObject;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Database\DBA;
@ -16,9 +18,21 @@ use Friendica\Database\DBA;
*/
class Group extends BaseObject
{
const FOLLOWERS = '~';
const MUTUALS = '&';
public static function getByUserId($uid, $includesDeleted = false)
{
$conditions = ['uid' => $uid];
if (!$includesDeleted) {
$conditions['deleted'] = false;
}
return DBA::selectToArray('group', [], $conditions);
}
/**
*
*
* @param int $group_id
* @return bool
* @throws \Exception
@ -76,8 +90,8 @@ class Group extends BaseObject
/**
* Update group information.
*
* @param int $id Group ID
* @param string $name Group name
* @param int $id Group ID
* @param string $name Group name
*
* @return bool Was the update successful?
* @throws \Exception
@ -96,14 +110,13 @@ class Group extends BaseObject
*/
public static function getIdsByContactId($cid)
{
$condition = ['contact-id' => $cid];
$stmt = DBA::select('group_member', ['gid'], $condition);
$return = [];
$stmt = DBA::select('group_member', ['gid'], ['contact-id' => $cid]);
while ($group = DBA::fetch($stmt)) {
$return[] = $group['gid'];
}
DBA::close($stmt);
return $return;
}
@ -170,8 +183,9 @@ class Group extends BaseObject
* @return boolean
* @throws \Exception
*/
public static function remove($gid) {
if (! $gid) {
public static function remove($gid)
{
if (!$gid) {
return false;
}
@ -215,14 +229,15 @@ class Group extends BaseObject
/**
* @brief Mark a group as deleted based on its name
*
* @deprecated Use Group::remove instead
*
* @param int $uid
* @param string $name
* @return bool
* @throws \Exception
* @deprecated Use Group::remove instead
*
*/
public static function removeByName($uid, $name) {
public static function removeByName($uid, $name)
{
$return = false;
if (!empty($uid) && !empty($name)) {
$gid = self::getIdByName($uid, $name);
@ -280,13 +295,13 @@ class Group extends BaseObject
/**
* @brief Removes a contact from a group based on its name
*
* @deprecated Use Group::removeMember instead
*
* @param int $uid
* @param string $name
* @param int $cid
* @return boolean
* @throws \Exception
* @deprecated Use Group::removeMember instead
*
*/
public static function removeMemberByName($uid, $name, $cid)
{
@ -300,23 +315,55 @@ class Group extends BaseObject
/**
* @brief Returns the combined list of contact ids from a group id list
*
* @param int $uid
* @param array $group_ids
* @param boolean $check_dead
* @return array
* @throws \Exception
*/
public static function expand($group_ids, $check_dead = false)
public static function expand($uid, array $group_ids, $check_dead = false)
{
if (!is_array($group_ids) || !count($group_ids)) {
return [];
}
$stmt = DBA::select('group_member', ['contact-id'], ['gid' => $group_ids]);
$return = [];
while($group_member = DBA::fetch($stmt)) {
$key = array_search(self::FOLLOWERS, $group_ids);
if ($key !== false) {
$followers = Contact::selectToArray(['id'], [
'uid' => $uid,
'rel' => [Contact::FOLLOWER, Contact::FRIEND],
'protocol' => Protocol::SUPPORT_PRIVATE,
]);
foreach ($followers as $follower) {
$return[] = $follower['id'];
}
unset($group_ids[$key]);
}
$key = array_search(self::MUTUALS, $group_ids);
if ($key !== false) {
$mutuals = Contact::selectToArray(['id'], [
'uid' => $uid,
'rel' => [Contact::FRIEND],
'protocol' => Protocol::SUPPORT_PRIVATE,
]);
foreach ($mutuals as $mutual) {
$return[] = $mutual['id'];
}
unset($group_ids[$key]);
}
$stmt = DBA::select('group_member', ['contact-id'], ['gid' => $group_ids]);
while ($group_member = DBA::fetch($stmt)) {
$return[] = $group_member['contact-id'];
}
DBA::close($stmt);
if ($check_dead) {
Contact::pruneUnavailable($return);
@ -332,12 +379,10 @@ class Group extends BaseObject
* @param int $gid An optional pre-selected group
* @param string $label An optional label of the list
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \Exception
*/
public static function displayGroupSelection($uid, $gid = 0, $label = '')
{
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid], ['order' => ['name']]);
$display_groups = [
[
'name' => '',
@ -345,6 +390,8 @@ class Group extends BaseObject
'selected' => ''
]
];
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid], ['order' => ['name']]);
while ($group = DBA::fetch($stmt)) {
$display_groups[] = [
'name' => $group['name'],
@ -352,7 +399,9 @@ class Group extends BaseObject
'selected' => $gid == $group['id'] ? 'true' : ''
];
}
Logger::log('groups: ' . print_r($display_groups, true));
DBA::close($stmt);
Logger::info('Got groups', $display_groups);
if ($label == '') {
$label = L10n::t('Default privacy group for new contacts');
@ -377,7 +426,7 @@ class Group extends BaseObject
* @param string $group_id
* @param int $cid
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \Exception
*/
public static function sidebarWidget($every = 'contact', $each = 'group', $editmode = 'standard', $group_id = '', $cid = 0)
{
@ -394,13 +443,12 @@ class Group extends BaseObject
]
];
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => local_user()], ['order' => ['name']]);
$member_of = [];
if ($cid) {
$member_of = self::getIdsByContactId($cid);
}
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => local_user()], ['order' => ['name']]);
while ($group = DBA::fetch($stmt)) {
$selected = (($group_id == $group['id']) ? ' group-selected' : '');
@ -423,6 +471,7 @@ class Group extends BaseObject
'ismember' => in_array($group['id'], $member_of),
];
}
DBA::close($stmt);
// Don't show the groups on the network page when there is only one
if ((count($display_groups) <= 2) && ($each == 'network')) {
@ -445,7 +494,6 @@ class Group extends BaseObject
'$form_security_token' => BaseModule::getFormSecurityToken('group_edit'),
]);
return $o;
}
}

View File

@ -1246,7 +1246,6 @@ class Item extends BaseObject
return $contact_id;
}
}
return $item['author-id'];
}
@ -1472,6 +1471,9 @@ class Item extends BaseObject
$item['plink'] = defaults($item, 'plink', System::baseUrl() . '/display/' . urlencode($item['guid']));
// The contact-id should be set before "self::insert" was called - but there seems to be issues sometimes
$item["contact-id"] = self::contactId($item);
$default = ['url' => $item['author-link'], 'name' => $item['author-name'],
'photo' => $item['author-avatar'], 'network' => $item['network']];
@ -1527,9 +1529,6 @@ class Item extends BaseObject
unset($item['causer-id']);
unset($item['causer-link']);
// The contact-id should be set before "self::insert" was called - but there seems to be issues sometimes
$item['contact-id'] = self::contactId($item);
if ($item['network'] == Protocol::PHANTOM) {
$item['network'] = Protocol::DFRN;
Logger::notice('Missing network, setting to {network}.', [
@ -2804,7 +2803,7 @@ class Item extends BaseObject
$replace = true;
}
} elseif ($item) {
if (self::samePermissions($item, $photo)) {
if (self::samePermissions($uid, $item, $photo)) {
$replace = true;
}
}
@ -2854,7 +2853,7 @@ class Item extends BaseObject
!empty($obj['deny_cid']) || !empty($obj['deny_gid']);
}
private static function samePermissions($obj1, $obj2)
private static function samePermissions($uid, $obj1, $obj2)
{
// first part is easy. Check that these are exactly the same.
if (($obj1['allow_cid'] == $obj2['allow_cid'])
@ -2875,12 +2874,12 @@ class Item extends BaseObject
}
// returns an array of contact-ids that are allowed to see this object
public static function enumeratePermissions($obj)
public static function enumeratePermissions(array $obj)
{
$allow_people = expand_acl($obj['allow_cid']);
$allow_groups = Group::expand(expand_acl($obj['allow_gid']));
$allow_groups = Group::expand($obj['uid'], expand_acl($obj['allow_gid']));
$deny_people = expand_acl($obj['deny_cid']);
$deny_groups = Group::expand(expand_acl($obj['deny_gid']));
$deny_groups = Group::expand($obj['uid'], expand_acl($obj['deny_gid']));
$recipients = array_unique(array_merge($allow_people, $allow_groups));
$deny = array_unique(array_merge($deny_people, $deny_groups));
$recipients = array_diff($recipients, $deny);

217
src/Module/Item/Compose.php Normal file
View File

@ -0,0 +1,217 @@
<?php
namespace Friendica\Module\Item;
use Friendica\BaseModule;
use Friendica\Content\Feature;
use Friendica\Core\Config;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\Database\DBA;
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', 'hidewall', 'default-location']);
switch ($posttype) {
case Item::PT_PERSONAL_NOTE:
$compose_title = L10n::t('Compose new personal note');
$type = 'note';
$doesFederate = false;
$contact_allow = $a->contact['id'];
$group_allow = '';
break;
default:
$compose_title = L10n::t('Compose new post');
$type = 'post';
$doesFederate = true;
$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::selectToArray(['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);
$jotnets_fields = [];
$mail_enabled = false;
$pubmail_enabled = false;
if (function_exists('imap_open') && !Config::get('system', 'imap_disabled')) {
$mailacct = DBA::selectFirst('mailacct', ['pubmail'], ['`uid` = ? AND `server` != ""', local_user()]);
if (DBA::isResult($mailacct)) {
$mail_enabled = true;
$pubmail_enabled = !empty($mailacct['pubmail']);
}
}
if (empty($user['hidewall'])) {
if ($mail_enabled) {
$jotnets_fields[] = [
'type' => 'checkbox',
'field' => [
'pubmail_enable',
L10n::t('Post to Email'),
$pubmail_enabled
]
];
}
Hook::callAll('jot_networks', $jotnets_fields);
}
$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)') : ''),
'$public_title' => L10n::t('Public'),
'$public_desc' => L10n::t('This post will be sent to all your followers and can be seen in the community pages and by anyone with its link.'),
'$custom_title' => L10n::t('Limited/Private'),
'$custom_desc' => L10n::t('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 appear anywhere public.'),
'$emailcc' => L10n::t('CC: email addresses'),
'$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,
'$doesFederate' => $doesFederate,
'$jotnets_fields'=> $jotnets_fields,
'$sourceapp' => L10n::t($a->sourcename),
'$rand_num' => Crypto::randomDigits(12)
]);
}
}

View File

@ -419,8 +419,8 @@ class Processor
$item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, true);
}
if (empty($item['contact-id'])) {
$item['contact-id'] = $item['author-id'];
if (($receiver != 0) && empty($item['contact-id'])) {
$item['contact-id'] = Contact::getIdForURL($activity['author'], 0, true);
}
if (!empty($activity['directmessage'])) {

View File

@ -271,9 +271,9 @@ class Notifier
}
$allow_people = expand_acl($parent['allow_cid']);
$allow_groups = Group::expand(expand_acl($parent['allow_gid']),true);
$allow_groups = Group::expand($uid, expand_acl($parent['allow_gid']),true);
$deny_people = expand_acl($parent['deny_cid']);
$deny_groups = Group::expand(expand_acl($parent['deny_gid']));
$deny_groups = Group::expand($uid, expand_acl($parent['deny_gid']));
// if our parent is a public forum (forum_mode == 1), uplink to the origional author causing
// a delivery fork. private groups (forum_mode == 2) do not uplink

View File

@ -5,6 +5,7 @@
namespace Friendica\Test;
use Friendica\Model\Group;
use PHPUnit\Framework\TestCase;
/**
@ -55,8 +56,8 @@ class TextTest extends TestCase
*/
public function testExpandAclNormal()
{
$text='<1><2><3>';
$this->assertEquals(array(1, 2, 3), expand_acl($text));
$text='<1><2><3><' . Group::FOLLOWERS . '><' . Group::MUTUALS . '>';
$this->assertEquals(array('1', '2', '3', Group::FOLLOWERS, Group::MUTUALS), expand_acl($text));
}
/**
@ -64,8 +65,8 @@ class TextTest extends TestCase
*/
public function testExpandAclBigNumber()
{
$text='<1><'.PHP_INT_MAX.'><15>';
$this->assertEquals(array(1, PHP_INT_MAX, 15), expand_acl($text));
$text='<1><' . PHP_INT_MAX . '><15>';
$this->assertEquals(array('1', (string)PHP_INT_MAX, '15'), expand_acl($text));
}
/**
@ -76,7 +77,7 @@ class TextTest extends TestCase
public function testExpandAclString()
{
$text="<1><279012><tt>";
$this->assertEquals(array(1, 279012), expand_acl($text));
$this->assertEquals(array('1', '279012'), expand_acl($text));
}
/**
@ -87,7 +88,7 @@ class TextTest extends TestCase
public function testExpandAclSpace()
{
$text="<1><279 012><32>";
$this->assertEquals(array(1, "279", "32"), expand_acl($text));
$this->assertEquals(array('1', '32'), expand_acl($text));
}
/**
@ -174,7 +175,7 @@ class TextTest extends TestCase
public function testExpandAclEmptyMatch()
{
$text="<1><><3>";
$this->assertEquals(array(1,3), expand_acl($text));
$this->assertEquals(array('1', '3'), expand_acl($text));
}
/**

View File

@ -632,7 +632,6 @@ function post_comment(id) {
unpause();
commentBusy = true;
$('body').css('cursor', 'wait');
$("#comment-preview-inp-" + id).val("0");
$.post(
"item",
$("#comment-edit-form-" + id).serialize(),
@ -661,11 +660,10 @@ function post_comment(id) {
}
function preview_comment(id) {
$("#comment-preview-inp-" + id).val("1");
$("#comment-edit-preview-" + id).show();
$.post(
"item",
$("#comment-edit-form-" + id).serialize(),
$("#comment-edit-form-" + id).serialize() + '&preview=1',
function(data) {
if (data.preview) {
$("#comment-edit-preview-" + id).html(data.preview);

View File

@ -10,7 +10,6 @@
<input type="hidden" name="parent" value="{{$parent}}" />
{{*<!--<input type="hidden" name="return" value="{{$return_path}}" />-->*}}
<input type="hidden" name="jsreload" value="{{$jsreload}}" />
<input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" />
<input type="hidden" name="post_id_random" value="{{$rand_num}}" />
<div class="comment-edit-photo" id="comment-edit-photo-{{$id}}" >

View File

@ -0,0 +1,251 @@
<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()
}
});
// Accessibility: enable space and enter to open a panel when focused
$('body').on('keyup', '[data-toggle="collapse"]:focus', function (e) {
if (e.key === ' ' || e.key === 'Enter') {
$(this).click();
e.preventDefault();
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);
$('.profile-jot-net input[type=checkbox]').each(function() {
// Restores checkbox state if it had been saved
if ($(this).attr('data-checked') !== undefined) {
$(this).prop('checked', $(this).attr('data-checked') === 'true');
}
});
$('.profile-jot-net input').attr('disabled', false);
});
$('#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);
$('.profile-jot-net input[type=checkbox]').each(function() {
// Saves current checkbox state
$(this)
.attr('data-checked', $(this).prop('checked'))
.prop('checked', false);
});
$('.profile-jot-net input').attr('disabled', 'disabled');
});
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,156 @@
<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="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" id="visibility-public-heading" role="button" data-toggle="collapse" data-parent="#visibility-accordion" href="#visibility-public-panel" aria-expanded="true" aria-controls="visibility-public-panel" tabindex="14">
<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_title}}
</label>
</div>
<div id="visibility-public-panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="visibility-public-heading">
<div class="panel-body">
<p>{{$public_desc}}</p>
{{if $doesFederate && $jotnets_fields}}
{{if $jotnets_fields|count < 3}}
<div class="profile-jot-net">
{{else}}
<details class="profile-jot-net">
<summary>{{$jotnets_summary}}</summary>
{{/if}}
{{foreach $jotnets_fields as $jotnets_field}}
{{if $jotnets_field.type == 'checkbox'}}
{{include file="field_checkbox.tpl" field=$jotnets_field.field}}
{{elseif $jotnets_field.type == 'select'}}
{{include file="field_select.tpl" field=$jotnets_field.field}}
{{/if}}
{{/foreach}}
{{if $jotnets_fields|count >= 3}}
</details>
{{else}}
</div>
{{/if}}
{{/if}}
</div>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading collapsed" id="visibility-custom-heading" role="button" data-toggle="collapse" data-parent="#visibility-accordion" href="#visibility-custom-panel" aria-expanded="true" aria-controls="visibility-custom-panel" tabindex="15">
<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_title}}
</label>
</div>
<div id="visibility-custom-panel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="visibility-custom-heading">
<div class="panel-body">
<p>{{$custom_desc}}</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>
{{if $doesFederate}}
<div class="form-group">
<label for="profile-jot-email" id="profile-jot-email-label">{{$emailcc}}</label>
<input type="text" name="emailcc" id="profile-jot-email" class="form-control" title="{{$emtitle}}" />
</div>
<div id="profile-jot-email-end"></div>
{{/if}}
<div class="jotplugins">
{{$jotplugins nofilter}}
</div>
{{/if}}
</form>
</div>
</div>

View File

@ -6,7 +6,6 @@
<input type="hidden" name="parent" value="{{$parent}}" />
<input type="hidden" name="return" value="{{$return_path}}" />
<input type="hidden" name="jsreload" value="{{$jsreload}}" />
<input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" />
<div class="comment-edit-photo" id="comment-edit-photo-{{$id}}">
<a class="comment-edit-photo-link" href="{{$mylink}}" title="{{$mytitle}}"><img class="my-comment-photo" src="{{$myphoto}}" alt="{{$mytitle}}" title="{{$mytitle}}" /></a>

View File

@ -10,7 +10,6 @@
<input type="hidden" name="parent" value="{{$parent}}" />
{{*<!--<input type="hidden" name="return" value="{{$return_path}}" />-->*}}
<input type="hidden" name="jsreload" value="{{$jsreload}}" />
<input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" />
<input type="hidden" name="post_id_random" value="{{$rand_num}}" />
<div class="comment-edit-photo" id="comment-edit-photo-{{$id}}">

View File

@ -6,7 +6,6 @@
<input type="hidden" name="parent" value="{{$parent}}" />
<input type="hidden" name="return" value="{{$return_path}}" />
<input type="hidden" name="jsreload" value="{{$jsreload}}" />
<input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" />
<div class="comment-edit-photo" id="comment-edit-photo-{{$id}}">
<a class="comment-edit-photo-link" href="{{$mylink}}" title="{{$mytitle}}"><img class="my-comment-photo" src="{{$myphoto}}" alt="{{$mytitle}}" title="{{$mytitle}}" /></a>

View File

@ -16,15 +16,16 @@ function theme_post(App $a)
}
if (isset($_POST['frio-settings-submit'])) {
PConfig::set(local_user(), 'frio', 'scheme', defaults($_POST, 'frio_scheme', ''));
PConfig::set(local_user(), 'frio', 'nav_bg', defaults($_POST, 'frio_nav_bg', ''));
PConfig::set(local_user(), 'frio', 'nav_icon_color', defaults($_POST, 'frio_nav_icon_color', ''));
PConfig::set(local_user(), 'frio', 'link_color', defaults($_POST, 'frio_link_color', ''));
PConfig::set(local_user(), 'frio', 'background_color', defaults($_POST, 'frio_background_color', ''));
PConfig::set(local_user(), 'frio', 'contentbg_transp', defaults($_POST, 'frio_contentbg_transp', ''));
PConfig::set(local_user(), 'frio', 'background_image', defaults($_POST, 'frio_background_image', ''));
PConfig::set(local_user(), 'frio', 'bg_image_option', defaults($_POST, 'frio_bg_image_option', ''));
PConfig::set(local_user(), 'frio', 'scheme', $_POST['frio_scheme'] ?? '');
PConfig::set(local_user(), 'frio', 'nav_bg', $_POST['frio_nav_bg'] ?? '');
PConfig::set(local_user(), 'frio', 'nav_icon_color', $_POST['frio_nav_icon_color'] ?? '');
PConfig::set(local_user(), 'frio', 'link_color', $_POST['frio_link_color'] ?? '');
PConfig::set(local_user(), 'frio', 'background_color', $_POST['frio_background_color'] ?? '');
PConfig::set(local_user(), 'frio', 'contentbg_transp', $_POST['frio_contentbg_transp'] ?? '');
PConfig::set(local_user(), 'frio', 'background_image', $_POST['frio_background_image'] ?? '');
PConfig::set(local_user(), 'frio', 'bg_image_option', $_POST['frio_bg_image_option'] ?? '');
PConfig::set(local_user(), 'frio', 'css_modified', time());
PConfig::set(local_user(), 'frio', 'enable_compose', $_POST['frio_enable_compose'] ?? 0);
}
}
@ -35,17 +36,18 @@ function theme_admin_post(App $a)
}
if (isset($_POST['frio-settings-submit'])) {
Config::set('frio', 'scheme', defaults($_POST, 'frio_scheme', ''));
Config::set('frio', 'nav_bg', defaults($_POST, 'frio_nav_bg', ''));
Config::set('frio', 'nav_icon_color', defaults($_POST, 'frio_nav_icon_color', ''));
Config::set('frio', 'link_color', defaults($_POST, 'frio_link_color', ''));
Config::set('frio', 'background_color', defaults($_POST, 'frio_background_color', ''));
Config::set('frio', 'contentbg_transp', defaults($_POST, 'frio_contentbg_transp', ''));
Config::set('frio', 'background_image', defaults($_POST, 'frio_background_image', ''));
Config::set('frio', 'bg_image_option', defaults($_POST, 'frio_bg_image_option', ''));
Config::set('frio', 'login_bg_image', defaults($_POST, 'frio_login_bg_image', ''));
Config::set('frio', 'login_bg_color', defaults($_POST, 'frio_login_bg_color', ''));
Config::set('frio', 'scheme', $_POST['frio_scheme'] ?? '');
Config::set('frio', 'nav_bg', $_POST['frio_nav_bg'] ?? '');
Config::set('frio', 'nav_icon_color', $_POST['frio_nav_icon_color'] ?? '');
Config::set('frio', 'link_color', $_POST['frio_link_color'] ?? '');
Config::set('frio', 'background_color', $_POST['frio_background_color'] ?? '');
Config::set('frio', 'contentbg_transp', $_POST['frio_contentbg_transp'] ?? '');
Config::set('frio', 'background_image', $_POST['frio_background_image'] ?? '');
Config::set('frio', 'bg_image_option', $_POST['frio_bg_image_option'] ?? '');
Config::set('frio', 'login_bg_image', $_POST['frio_login_bg_image'] ?? '');
Config::set('frio', 'login_bg_color', $_POST['frio_login_bg_color'] ?? '');
Config::set('frio', 'css_modified', time());
Config::set('frio', 'enable_compose', $_POST['frio_enable_compose'] ?? 0);
}
}
@ -67,6 +69,7 @@ function theme_content(App $a)
$arr['contentbg_transp'] = PConfig::get(local_user(), 'frio', 'contentbg_transp', Config::get('frio', 'contentbg_transp'));
$arr['background_image'] = PConfig::get(local_user(), 'frio', 'background_image', Config::get('frio', 'background_image'));
$arr['bg_image_option'] = PConfig::get(local_user(), 'frio', 'bg_image_option' , Config::get('frio', 'bg_image_option'));
$arr['enable_compose'] = PConfig::get(local_user(), 'frio', 'enable_compose' , Config::get('frio', 'enable_compose'));
return frio_form($arr);
}
@ -78,7 +81,7 @@ function theme_admin(App $a)
}
$arr = [];
$arr['scheme'] = Config::get('frio', 'scheme', Config::get('frio', 'scheme'));
$arr['scheme'] = Config::get('frio', 'scheme', Config::get('frio', 'schema'));
$arr['share_string'] = '';
$arr['nav_bg'] = Config::get('frio', 'nav_bg');
$arr['nav_icon_color'] = Config::get('frio', 'nav_icon_color');
@ -89,6 +92,7 @@ function theme_admin(App $a)
$arr['bg_image_option'] = Config::get('frio', 'bg_image_option');
$arr['login_bg_image'] = Config::get('frio', 'login_bg_image');
$arr['login_bg_color'] = Config::get('frio', 'login_bg_color');
$arr['enable_compose'] = Config::get('frio', 'enable_compose');
return frio_form($arr);
}
@ -132,6 +136,7 @@ function frio_form($arr)
'$background_image' => array_key_exists('background_image', $disable) ? '' : ['frio_background_image', L10n::t('Set the background image'), $arr['background_image'], $background_image_help, false],
'$bg_image_options_title' => L10n::t('Background image style'),
'$bg_image_options' => Image::get_options($arr),
'$enable_compose' => ['frio_enable_compose', L10n::t('Enable Compose page'), $arr['enable_compose'], L10n::t('This replaces the jot modal window for writing new posts with a link to <a href="compose">the new Compose page</a>.')],
];
if (array_key_exists('login_bg_image', $arr) && !array_key_exists('login_bg_image', $disable)) {

View File

@ -192,7 +192,6 @@ blockquote {
background-image: none;
text-shadow: none;
border-radius: 3px;
outline: 0!important;
margin-bottom: 0;
font-size: 14px;
font-weight: 600;
@ -1314,7 +1313,8 @@ section ul.tabs {
section #jotOpen {
display: none;
}
#jotOpen {
#jotOpen,
#composeOpen {
margin-top: 3px;
float: right;
}
@ -1372,7 +1372,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;
@ -1383,7 +1384,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;
}
@ -3425,7 +3427,6 @@ section .profile-match-wrapper {
background-image: none;
text-shadow: none;
border-radius: 3px;
outline: 0!important;
margin-bottom: 0;
font-size: 14px;
font-weight: 600;
@ -3444,7 +3445,6 @@ section .profile-match-wrapper {
background-image: none;
text-shadow: none;
border-radius: 3px;
outline: 0!important;
margin-bottom: 0;
font-size: 14px;
font-weight: 600;

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Tim Schlechter
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,54 @@
/*
* friendica-tagsinput v0.8.0
*
*/
.twitter-typeahead .tt-query,
.twitter-typeahead .tt-hint {
margin-bottom: 0;
}
.twitter-typeahead .tt-hint
{
display: none;
}
.tt-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
font-size: 14px;
background-color: #ffffff;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
cursor: pointer;
}
.tt-suggestion {
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 1.428571429;
color: #333333;
white-space: nowrap;
}
.tt-suggestion:hover,
.tt-suggestion:focus {
color: #ffffff;
text-decoration: none;
outline: 0;
background-color: #428bca;
}

View File

@ -0,0 +1,72 @@
/*
* friendica-tagsinput v0.8.0
*
*/
.friendica-tagsinput {
background-color: #fff;
border: 1px solid #ccc;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
display: inline-block;
padding: 4px 6px;
color: #555;
vertical-align: middle;
border-radius: 4px;
max-width: 100%;
line-height: 22px;
cursor: text;
height: auto;
}
.friendica-tagsinput.input-lg {
line-height: 27px;
}
.friendica-tagsinput input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0 6px;
margin: 0;
width: auto;
max-width: inherit;
}
.friendica-tagsinput.form-control input::-moz-placeholder {
color: #777;
opacity: 1;
}
.friendica-tagsinput.form-control input:-ms-input-placeholder {
color: #777;
}
.friendica-tagsinput.form-control input::-webkit-input-placeholder {
color: #777;
}
.friendica-tagsinput input:focus {
border: none;
box-shadow: none;
}
.friendica-tagsinput .tag {
margin: 0 2px 2px 0;
color: white;
font-weight: normal;
}
.friendica-tagsinput .tag img {
width: auto;
height: 1.5em;
vertical-align: text-top;
margin-right: 8px;
}
.friendica-tagsinput .tag [data-role="remove"] {
margin-left: 8px;
cursor: pointer;
}
.friendica-tagsinput .tag [data-role="remove"]:after {
content: "x";
padding: 0px 2px;
font-weight: bold;
}
.friendica-tagsinput .tag [data-role="remove"]:hover {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
}
.friendica-tagsinput .tag [data-role="remove"]:hover:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}

View File

@ -0,0 +1,695 @@
/*
* friendica-tagsinput v0.8.0
* Based on bootstrap-tagsinput v0.8.0
*
* Adds:
* - optional thumbnail
* - copying source input element class to the pseudo-input element
*
*/
(function ($) {
"use strict";
var defaultOptions = {
tagClass: function(item) {
return 'label label-info';
},
focusClass: 'focus',
itemValue: function(item) {
return item ? item.toString() : item;
},
itemText: function(item) {
return this.itemValue(item);
},
itemTitle: function(item) {
return null;
},
itemThumb: function(item) {
return null;
},
freeInput: true,
addOnBlur: true,
maxTags: undefined,
maxChars: undefined,
confirmKeys: [13, 44],
delimiter: ',',
delimiterRegex: null,
cancelConfirmKeysOnEmpty: false,
onTagExists: function(item, $tag) {
$tag.hide().fadeIn();
},
trimValue: false,
allowDuplicates: false,
triggerChange: true
};
/**
* Constructor function
*/
function TagsInput(element, options) {
this.isInit = true;
this.itemsArray = [];
this.$element = $(element);
this.$element.hide();
this.isSelect = (element.tagName === 'SELECT');
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
this.objectItems = options && options.itemValue;
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
this.inputSize = Math.max(1, this.placeholderText.length);
this.$container = $('<div class="friendica-tagsinput"></div>');
this.$container.addClass(this.$element.attr('class'));
this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
this.$element.before(this.$container);
this.build(options);
this.isInit = false;
}
TagsInput.prototype = {
constructor: TagsInput,
/**
* Adds the given item as a new tag. Pass true to dontPushVal to prevent
* updating the elements val()
*/
add: function(item, dontPushVal, options) {
let self = this;
if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
return;
// Ignore falsey values, except false
if (item !== false && !item)
return;
// Trim value
if (typeof item === "string" && self.options.trimValue) {
item = $.trim(item);
}
// Throw an error when trying to add an object while the itemValue option was not set
if (typeof item === "object" && !self.objectItems)
throw("Can't add objects when itemValue option is not set");
// Ignore strings only containg whitespace
if (item.toString().match(/^\s*$/))
return;
// If SELECT but not multiple, remove current tag
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
self.remove(self.itemsArray[0]);
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter;
var items = item.split(delimiter);
if (items.length > 1) {
for (var i = 0; i < items.length; i++) {
this.add(items[i], true);
}
if (!dontPushVal)
self.pushVal(self.options.triggerChange);
return;
}
}
var itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item),
itemTitle = self.options.itemTitle(item),
itemThumb = self.options.itemThumb(item);
// Ignore items allready added
var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
if (existing && !self.options.allowDuplicates) {
// Invoke onTagExists
if (self.options.onTagExists) {
var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
self.options.onTagExists(item, $existingTag);
}
return;
}
// if length greater than limit
if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
return;
// raise beforeItemAdd arg
var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options});
self.$element.trigger(beforeItemAddEvent);
if (beforeItemAddEvent.cancel)
return;
// register item in internal array and map
self.itemsArray.push(item);
// add a tag element
var $tag = $('<span class="tag ' + htmlEncode(tagClass) + (itemTitle !== null ? ('" title="' + itemTitle) : '') + '">' +
(itemThumb !== null ? '<img src="' + itemThumb + '" alt="">' : '') +
htmlEncode(itemText) + '<span data-role="remove"></span>' +
'</span>');
$tag.data('item', item);
self.findInputWrapper().before($tag);
$tag.after(' ');
// Check to see if the tag exists in its raw or uri-encoded form
var optionExists = (
$('option[value="' + encodeURIComponent(itemValue) + '"]', self.$element).length ||
$('option[value="' + htmlEncode(itemValue) + '"]', self.$element).length
);
// add <option /> if item represents a value not present in one of the <select />'s options
if (self.isSelect && !optionExists) {
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
$option.data('item', item);
$option.attr('value', itemValue);
self.$element.append($option);
}
if (!dontPushVal)
self.pushVal(self.options.triggerChange);
// Add class when reached maxTags
if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
self.$container.addClass('friendica-tagsinput-max');
// If using typeahead, once the tag has been added, clear the typeahead value so it does not stick around in the input.
if ($('.typeahead, .twitter-typeahead', self.$container).length) {
self.$input.typeahead('val', '');
}
if (this.isInit) {
self.$element.trigger($.Event('itemAddedOnInit', { item: item, options: options }));
} else {
self.$element.trigger($.Event('itemAdded', { item: item, options: options }));
}
},
/**
* Removes the given item. Pass true to dontPushVal to prevent updating the
* elements val()
*/
remove: function(item, dontPushVal, options) {
var self = this;
if (self.objectItems) {
if (typeof item === "object")
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
else
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
item = item[item.length-1];
}
if (item) {
var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options });
self.$element.trigger(beforeItemRemoveEvent);
if (beforeItemRemoveEvent.cancel)
return;
$('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
if($.inArray(item, self.itemsArray) !== -1)
self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
}
if (!dontPushVal)
self.pushVal(self.options.triggerChange);
// Remove class when reached maxTags
if (self.options.maxTags > self.itemsArray.length)
self.$container.removeClass('friendica-tagsinput-max');
self.$element.trigger($.Event('itemRemoved', { item: item, options: options }));
},
/**
* Removes all items
*/
removeAll: function() {
var self = this;
$('.tag', self.$container).remove();
$('option', self.$element).remove();
while(self.itemsArray.length > 0)
self.itemsArray.pop();
self.pushVal(self.options.triggerChange);
},
/**
* Refreshes the tags so they match the text/value of their corresponding
* item.
*/
refresh: function() {
var self = this;
$('.tag', self.$container).each(function() {
var $tag = $(this),
item = $tag.data('item'),
itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item);
// Update tag's class and inner text
$tag.attr('class', null);
$tag.addClass('tag ' + htmlEncode(tagClass));
$tag.contents().filter(function() {
return this.nodeType == 3;
})[0].nodeValue = htmlEncode(itemText);
if (self.isSelect) {
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
option.attr('value', itemValue);
}
});
},
/**
* Returns the items added as tags
*/
items: function() {
return this.itemsArray;
},
/**
* Assembly value by retrieving the value of each item, and set it on the
* element.
*/
pushVal: function() {
var self = this,
val = $.map(self.items(), function(item) {
return self.options.itemValue(item).toString();
});
self.$element.val(val, true);
if (self.options.triggerChange)
self.$element.trigger('change');
},
/**
* Initializes the tags input behaviour on the element
*/
build: function(options) {
var self = this;
self.options = $.extend({}, defaultOptions, options);
// When itemValue is set, freeInput should always be false
if (self.objectItems)
self.options.freeInput = false;
makeOptionItemFunction(self.options, 'itemValue');
makeOptionItemFunction(self.options, 'itemText');
makeOptionItemFunction(self.options, 'itemThumb');
makeOptionFunction(self.options, 'tagClass');
// Typeahead Bootstrap version 2.3.2
if (self.options.typeahead) {
var typeahead = self.options.typeahead || {};
makeOptionFunction(typeahead, 'source');
self.$input.typeahead($.extend({}, typeahead, {
source: function (query, process) {
function processItems(items) {
var texts = [];
for (var i = 0; i < items.length; i++) {
var text = self.options.itemText(items[i]);
map[text] = items[i];
texts.push(text);
}
process(texts);
}
this.map = {};
var map = this.map,
data = typeahead.source(query);
if ($.isFunction(data.success)) {
// support for Angular callbacks
data.success(processItems);
} else if ($.isFunction(data.then)) {
// support for Angular promises
data.then(processItems);
} else {
// support for functions and jquery promises
$.when(data)
.then(processItems);
}
},
updater: function (text) {
self.add(this.map[text]);
return this.map[text];
},
matcher: function (text) {
return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
},
sorter: function (texts) {
return texts.sort();
},
highlighter: function (text) {
var regex = new RegExp( '(' + this.query + ')', 'gi' );
return text.replace( regex, "<strong>$1</strong>" );
}
}));
}
// typeahead.js
if (self.options.typeaheadjs) {
var typeaheadConfig = null;
var typeaheadDatasets = {};
// Determine if main configurations were passed or simply a dataset
var typeaheadjs = self.options.typeaheadjs;
if ($.isArray(typeaheadjs)) {
typeaheadConfig = typeaheadjs[0];
typeaheadDatasets = typeaheadjs[1];
} else {
typeaheadDatasets = typeaheadjs;
}
self.$input.typeahead(typeaheadConfig, typeaheadDatasets).on('typeahead:selected', $.proxy(function (obj, datum) {
if (typeaheadDatasets.valueKey)
self.add(datum[typeaheadDatasets.valueKey]);
else
self.add(datum);
self.$input.typeahead('val', '');
}, self));
}
self.$container.on('click', $.proxy(function(event) {
if (! self.$element.attr('disabled')) {
self.$input.removeAttr('disabled');
}
self.$input.focus();
}, self));
if (self.options.addOnBlur && self.options.freeInput) {
self.$input.on('focusout', $.proxy(function(event) {
// HACK: only process on focusout when no typeahead opened, to
// avoid adding the typeahead text as tag
if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
self.add(self.$input.val());
self.$input.val('');
}
}, self));
}
// Toggle the 'focus' css class on the container when it has focus
self.$container.on({
focusin: function() {
self.$container.addClass(self.options.focusClass);
},
focusout: function() {
self.$container.removeClass(self.options.focusClass);
},
});
self.$container.on('keydown', 'input', $.proxy(function(event) {
var $input = $(event.target),
$inputWrapper = self.findInputWrapper();
if (self.$element.attr('disabled')) {
self.$input.attr('disabled', 'disabled');
return;
}
switch (event.which) {
// BACKSPACE
case 8:
if (doGetCaretPosition($input[0]) === 0) {
var prev = $inputWrapper.prev();
if (prev.length) {
self.remove(prev.data('item'));
}
}
break;
// DELETE
case 46:
if (doGetCaretPosition($input[0]) === 0) {
var next = $inputWrapper.next();
if (next.length) {
self.remove(next.data('item'));
}
}
break;
// LEFT ARROW
case 37:
// Try to move the input before the previous tag
var $prevTag = $inputWrapper.prev();
if ($input.val().length === 0 && $prevTag[0]) {
$prevTag.before($inputWrapper);
$input.focus();
}
break;
// RIGHT ARROW
case 39:
// Try to move the input after the next tag
var $nextTag = $inputWrapper.next();
if ($input.val().length === 0 && $nextTag[0]) {
$nextTag.after($inputWrapper);
$input.focus();
}
break;
default:
// ignore
}
// Reset internal input's size
var textLength = $input.val().length,
wordSpace = Math.ceil(textLength / 5),
size = textLength + wordSpace + 1;
$input.attr('size', Math.max(this.inputSize, $input.val().length));
}, self));
self.$container.on('keypress', 'input', $.proxy(function(event) {
var $input = $(event.target);
if (self.$element.attr('disabled')) {
self.$input.attr('disabled', 'disabled');
return;
}
var text = $input.val(),
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
// Only attempt to add a tag if there is data in the field
if (text.length !== 0) {
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
$input.val('');
}
// If the field is empty, let the event triggered fire as usual
if (self.options.cancelConfirmKeysOnEmpty === false) {
event.preventDefault();
}
}
// Reset internal input's size
var textLength = $input.val().length,
wordSpace = Math.ceil(textLength / 5),
size = textLength + wordSpace + 1;
$input.attr('size', Math.max(this.inputSize, $input.val().length));
}, self));
// Remove icon clicked
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
if (self.$element.attr('disabled')) {
return;
}
self.remove($(event.target).closest('.tag').data('item'));
}, self));
// Only add existing value as tags when using strings as tags
if (self.options.itemValue === defaultOptions.itemValue) {
if (self.$element[0].tagName === 'INPUT') {
self.add(self.$element.val());
} else {
$('option', self.$element).each(function() {
self.add($(this).attr('value'), true);
});
}
}
},
/**
* Removes all tagsinput behaviour and unregsiter all event handlers
*/
destroy: function() {
var self = this;
// Unbind events
self.$container.off('keypress', 'input');
self.$container.off('click', '[role=remove]');
self.$container.remove();
self.$element.removeData('tagsinput');
self.$element.show();
},
/**
* Sets focus on the tagsinput
*/
focus: function() {
this.$input.focus();
},
/**
* Returns the internal input element
*/
input: function() {
return this.$input;
},
/**
* Returns the element which is wrapped around the internal input. This
* is normally the $container, but typeahead.js moves the $input element.
*/
findInputWrapper: function() {
var elt = this.$input[0],
container = this.$container[0];
while(elt && elt.parentNode !== container)
elt = elt.parentNode;
return $(elt);
}
};
/**
* Register JQuery plugin
*/
$.fn.tagsinput = function(arg1, arg2, arg3) {
var results = [];
this.each(function() {
var tagsinput = $(this).data('tagsinput');
// Initialize a new tags input
if (!tagsinput) {
tagsinput = new TagsInput(this, arg1);
$(this).data('tagsinput', tagsinput);
results.push(tagsinput);
if (this.tagName === 'SELECT') {
$('option', $(this)).attr('selected', 'selected');
}
// Init tags from $(this).val()
$(this).val($(this).val());
} else if (!arg1 && !arg2) {
// tagsinput already exists
// no function, trying to init
results.push(tagsinput);
} else if(tagsinput[arg1] !== undefined) {
// Invoke function on existing tags input
if(tagsinput[arg1].length === 3 && arg3 !== undefined){
var retVal = tagsinput[arg1](arg2, null, arg3);
}else{
var retVal = tagsinput[arg1](arg2);
}
if (retVal !== undefined)
results.push(retVal);
}
});
if ( typeof arg1 == 'string') {
// Return the results from the invoked function calls
return results.length > 1 ? results : results[0];
} else {
return results;
}
};
$.fn.tagsinput.Constructor = TagsInput;
/**
* Most options support both a string or number as well as a function as
* option value. This function makes sure that the option with the given
* key in the given options is wrapped in a function
*/
function makeOptionItemFunction(options, key) {
if (typeof options[key] !== 'function') {
var propertyName = options[key];
options[key] = function(item) { return item[propertyName]; };
}
}
function makeOptionFunction(options, key) {
if (typeof options[key] !== 'function') {
var value = options[key];
options[key] = function() { return value; };
}
}
/**
* HtmlEncodes the given value
*/
var htmlEncodeContainer = $('<div />');
function htmlEncode(value) {
if (value) {
return htmlEncodeContainer.text(value).html();
} else {
return '';
}
}
/**
* Returns the position of the caret in the given input field
* http://flightschool.acylt.com/devnotes/caret-position-woes/
*/
function doGetCaretPosition(oField) {
var iCaretPos = 0;
if (document.selection) {
oField.focus ();
var oSel = document.selection.createRange();
oSel.moveStart ('character', -oField.value.length);
iCaretPos = oSel.text.length;
} else if (oField.selectionStart || oField.selectionStart == '0') {
iCaretPos = oField.selectionStart;
}
return (iCaretPos);
}
/**
* Returns boolean indicates whether user has pressed an expected key combination.
* @param object keyPressEvent: JavaScript event object, refer
* http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
* @param object lookupList: expected key combinations, as in:
* [13, {which: 188, shiftKey: true}]
*/
function keyCombinationInList(keyPressEvent, lookupList) {
var found = false;
$.each(lookupList, function (index, keyCombination) {
if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
found = true;
return false;
}
if (keyPressEvent.which === keyCombination.which) {
var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
if (alt && shift && ctrl) {
found = true;
return false;
}
}
});
return found;
}
/**
* Initialize tagsinput behaviour on inputs and selects which have
* data-role=tagsinput
*/
$(function() {
$("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
});
})(window.jQuery);

View File

@ -64,9 +64,24 @@ $(document).ready(function(){
});
// add Jot botton to the scecond navbar
if( $("section #jotOpen").length ) {
$("section #jotOpen").appendTo("#topbar-second > .container > #navbar-button");
if( $("#jot-popup").is(":hidden")) $("#topbar-second > .container > #navbar-button #jotOpen").hide();
let $jotButton = $("#jotOpen");
let $composeButton = $("#composeOpen");
if (compose) {
$jotButton.hide();
if ($composeButton.length) {
$composeButton.appendTo("#topbar-second > .container > #navbar-button");
if($("#jot-popup").is(":hidden")) {
$composeButton.hide();
}
}
} else {
$composeButton.hide();
if ($jotButton.length) {
$jotButton.appendTo("#topbar-second > .container > #navbar-button");
if($("#jot-popup").is(":hidden")) {
$jotButton.hide();
}
}
}
// show bulk deletion button at network page if checkbox is checked

View File

@ -10,7 +10,6 @@
<input type="hidden" name="parent" value="{{$parent}}" />
{{*<!--<input type="hidden" name="return" value="{{$return_path}}" />-->*}}
<input type="hidden" name="jsreload" value="{{$jsreload}}" />
<input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" />
<input type="hidden" name="post_id_random" value="{{$rand_num}}" />
<p class="comment-edit-bb-{{$id}} comment-icon-list">

View File

@ -1,6 +1,6 @@
{{* The button to open the jot - in This theme we move the button with js to the second nav bar *}}
<button class="btn btn-sm btn-main pull-right" id="jotOpen" aria-label="{{$new_post}}" title="{{$new_post}}" onclick="jotShow();"><i class="fa fa-pencil-square-o fa-2x"></i></button>
<a class="btn btn-sm btn-main pull-right" id="composeOpen" href="compose/{{$posttype}}{{if $content}}?body={{$content}}{{/if}}" aria-label="{{$new_post}}" title="{{$new_post}}"><i class="fa fa-pencil-square-o fa-2x"></i></a>
<div id="jot-content">
<div id="jot-sections">

View File

@ -183,6 +183,10 @@
});
</script>
<div class="form-group">
{{include file="field_checkbox.tpl" field=$enable_compose}}
</div>
<div class="settings-submit-wrapper form-group pull-right">
<button type="submit" value="{{$submit}}" class="settings-submit btn btn-primary" name="frio-settings-submit">{{$submit}}</button>
</div>

View File

@ -41,6 +41,14 @@ function frio_init(App $a)
</script>
EOT;
}
$enable_compose = \Friendica\Core\PConfig::get(local_user(), 'frio', 'enable_compose');
$compose = $enable_compose === '1' || $enable_compose === null && Config::get('frio', 'enable_compose') ? 1 : 0;
$a->page['htmlhead'] .= <<< HTML
<script type="text/javascript">
var compose = $compose;
</script>
HTML;
}
function frio_install()

View File

@ -5,7 +5,6 @@
<input type="hidden" name="parent" value="{{$parent}}" />
{{*<!--<input type="hidden" name="return" value="{{$return_path}}" />-->*}}
<input type="hidden" name="jsreload" value="{{$jsreload}}" />
<input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" />
<input type="hidden" name="post_id_random" value="{{$rand_num}}" />
<div class="comment-edit-photo" id="comment-edit-photo-{{$id}}">

View File

@ -10,7 +10,6 @@
<input type="hidden" name="parent" value="{{$parent}}" />
{{*<!--<input type="hidden" name="return" value="{{$return_path}}" />-->*}}
<input type="hidden" name="jsreload" value="{{$jsreload}}" />
<input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" />
<input type="hidden" name="post_id_random" value="{{$rand_num}}" />
<div class="comment-edit-photo" id="comment-edit-photo-{{$id}}">