diff options
-rw-r--r-- | Zotlabs/Lib/Enotify.php | 136 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadItem.php | 5 | ||||
-rw-r--r-- | Zotlabs/Module/Mail.php | 2 | ||||
-rw-r--r-- | Zotlabs/Module/Sse.php | 108 | ||||
-rw-r--r-- | Zotlabs/Module/Sse_bs.php | 519 | ||||
-rwxr-xr-x | boot.php | 60 | ||||
-rw-r--r-- | doc/hooklist.bb | 3 | ||||
-rw-r--r-- | include/channel.php | 5 | ||||
-rw-r--r-- | include/event.php | 12 | ||||
-rw-r--r-- | view/css/widgets.css | 5 | ||||
-rw-r--r-- | view/de-de/hstrings.php | 2 | ||||
-rw-r--r-- | view/js/main.js | 531 | ||||
-rwxr-xr-x | view/js/sse_worker.js | 14 | ||||
-rw-r--r-- | view/php/theme_init.php | 1 | ||||
-rw-r--r-- | view/tpl/activity_filter_widget.tpl | 2 | ||||
-rwxr-xr-x | view/tpl/conv_item.tpl | 4 | ||||
-rwxr-xr-x | view/tpl/conv_list.tpl | 2 | ||||
-rwxr-xr-x | view/tpl/js_strings.tpl | 2 | ||||
-rw-r--r-- | view/tpl/notifications_widget.tpl | 28 |
19 files changed, 1378 insertions, 63 deletions
diff --git a/Zotlabs/Lib/Enotify.php b/Zotlabs/Lib/Enotify.php index 92a488f67..de2bfba24 100644 --- a/Zotlabs/Lib/Enotify.php +++ b/Zotlabs/Lib/Enotify.php @@ -550,6 +550,11 @@ class Enotify { if ((\App::$language === 'en' || (! \App::$language)) && strpos($msg,', ')) $msg = substr($msg,strpos($msg,', ')+1); + $datarray['id'] = $notify_id; + $datarray['msg'] = $msg; + + call_hooks('enotify_store_end', $datarray); + $r = q("update notify set msg = '%s' where id = %d and uid = %d", dbesc($msg), intval($notify_id), @@ -838,7 +843,7 @@ class Enotify { 'addr' => (($item[$who]['xchan_addr']) ? $item[$who]['xchan_addr'] : $item[$who]['xchan_url']), 'url' => $item[$who]['xchan_url'], 'photo' => $item[$who]['xchan_photo_s'], - 'when' => relative_date(($edit)? $item['edited'] : $item['created']), + 'when' => (($edit) ? datetime_convert('UTC', date_default_timezone_get(), $item['edited']) : datetime_convert('UTC', date_default_timezone_get(), $item['created'])), 'class' => (intval($item['item_unseen']) ? 'notify-unseen' : 'notify-seen'), 'b64mid' => ((in_array($item['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) ? 'b64.' . base64url_encode($item['thr_parent']) : 'b64.' . base64url_encode($item['mid'])), 'notify_id' => 'undefined', @@ -846,7 +851,7 @@ class Enotify { 'message' => strip_tags(bbcode($itemem_text)), // these are for the superblock addon 'hash' => $item[$who]['xchan_hash'], - 'uid' => local_channel(), + 'uid' => $item['uid'], 'display' => true ); @@ -858,4 +863,131 @@ class Enotify { return $x; } + static public function format_notify($tt) { + + $message = trim(strip_tags(bbcode($tt['msg']))); + + if(strpos($message, $tt['xname']) === 0) + $message = substr($message, strlen($tt['xname']) + 1); + + $mid = basename($tt['link']); + $mid = ((strpos($mid, 'b64.') === 0) ? @base64url_decode(substr($mid, 4)) : $mid); + + if(in_array($tt['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) { + // we need the thread parent + $r = q("select thr_parent from item where mid = '%s' and uid = %d limit 1", + dbesc($mid), + intval(local_channel()) + ); + $b64mid = ((strpos($r[0]['thr_parent'], 'b64.') === 0) ? $r[0]['thr_parent'] : 'b64.' . base64url_encode($r[0]['thr_parent'])); + } + else { + $b64mid = ((strpos($mid, 'b64.') === 0) ? $mid : 'b64.' . base64url_encode($mid)); + } + + $x = [ + 'notify_link' => z_root() . '/notify/view/' . $tt['id'], + 'name' => $tt['xname'], + 'url' => $tt['url'], + 'photo' => $tt['photo'], + 'when' => datetime_convert('UTC', date_default_timezone_get(), $tt['created']), + 'hclass' => (($tt['seen']) ? 'notify-seen' : 'notify-unseen'), + 'b64mid' => (($tt['otype'] == 'item') ? $b64mid : 'undefined'), + 'notify_id' => (($tt['otype'] == 'item') ? $tt['id'] : 'undefined'), + 'message' => $message + ]; + + return $x; + + } + + static public function format_intros($rr) { + + $x = [ + 'notify_link' => z_root() . '/connections/ifpending', + 'name' => $rr['xchan_name'], + 'addr' => $rr['xchan_addr'], + 'url' => $rr['xchan_url'], + 'photo' => $rr['xchan_photo_s'], + 'when' => datetime_convert('UTC', date_default_timezone_get(), $rr['abook_created']), + 'hclass' => ('notify-unseen'), + 'message' => t('added your channel') + ]; + + return $x; + + } + + static public function format_files($rr) { + + $x = [ + 'notify_link' => z_root() . '/sharedwithme', + 'name' => $rr['author']['xchan_name'], + 'addr' => $rr['author']['xchan_addr'], + 'url' => $rr['author']['xchan_url'], + 'photo' => $rr['author']['xchan_photo_s'], + 'when' => datetime_convert('UTC', date_default_timezone_get(), $rr['created']), + 'hclass' => ('notify-unseen'), + 'message' => t('shared a file with you') + ]; + + return $x; + + } + + static public function format_mail($rr) { + + $x = [ + 'notify_link' => z_root() . '/mail/' . $rr['id'], + 'name' => $rr['xchan_name'], + 'addr' => $rr['xchan_addr'], + 'url' => $rr['xchan_url'], + 'photo' => $rr['xchan_photo_s'], + 'when' => datetime_convert('UTC', date_default_timezone_get(), $rr['created']), + 'hclass' => (intval($rr['mail_seen']) ? 'notify-seen' : 'notify-unseen'), + 'message' => t('sent you a private message'), + ]; + + return $x; + + } + + static public function format_all_events($rr) { + + $bd_format = t('g A l F d') ; // 8 AM Friday January 18 + $strt = datetime_convert('UTC', (($rr['adjust']) ? date_default_timezone_get() : 'UTC'), $rr['dtstart']); + $today = ((substr($strt, 0, 10) === datetime_convert('UTC', date_default_timezone_get(), 'now', 'Y-m-d')) ? true : false); + $when = day_translate(datetime_convert('UTC', (($rr['adjust']) ? date_default_timezone_get() : 'UTC'), $rr['dtstart'], $bd_format)) . (($today) ? ' ' . t('[today]') : ''); + + $x = [ + 'notify_link' => z_root() . '/cdav/calendar/' . $rr['event_hash'], + 'name' => $rr['xchan_name'], + 'addr' => $rr['xchan_addr'], + 'url' => $rr['xchan_url'], + 'photo' => $rr['xchan_photo_s'], + 'when' => $when, + 'hclass' => ('notify-unseen'), + 'message' => t('posted an event') + ]; + + return $x; + + } + + static public function format_register($rr) { + + $x = [ + 'notify_link' => z_root() . '/admin/accounts', + 'name' => $rr['account_email'], + 'addr' => $rr['account_email'], + 'url' => '', + 'photo' => z_root() . '/' . get_default_profile_photo(48), + 'when' => datetime_convert('UTC', date_default_timezone_get(),$rr['account_created']), + 'hclass' => ('notify-unseen'), + 'message' => t('requires approval') + ]; + + return $x; + + } } diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index 5e4600df2..174af7f0e 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -369,7 +369,7 @@ class ThreadItem { 'folders' => $body['folders'], 'text' => strip_tags($body['html']), 'id' => $this->get_id(), - 'mid' => $item['mid'], + 'mid' => 'b64.' . base64url_encode($item['mid']), 'parent' => $item['parent'], 'author_id' => (($item['author']['xchan_addr']) ? $item['author']['xchan_addr'] : $item['author']['xchan_url']), 'isevent' => $isevent, @@ -467,10 +467,9 @@ class ThreadItem { 'previewing' => ($conv->is_preview() ? true : false ), 'preview_lbl' => t('This is an unsaved preview'), 'wait' => t('Please wait'), - 'submid' => str_replace(['+','='], ['',''], base64_encode($item['mid'])), 'thread_level' => $thread_level, 'settings' => $settings, - 'thr_parent' => (($item['parent_mid'] != $item['thr_parent']) ? $item['thr_parent'] : '') + 'thr_parent' => (($item['parent_mid'] != $item['thr_parent']) ? 'b64.' . base64url_encode($item['thr_parent']) : '') ); $arr = array('item' => $item, 'output' => $tmp_item); diff --git a/Zotlabs/Module/Mail.php b/Zotlabs/Module/Mail.php index 7c344966b..636fc4e33 100644 --- a/Zotlabs/Module/Mail.php +++ b/Zotlabs/Module/Mail.php @@ -145,7 +145,7 @@ class Mail extends \Zotlabs\Web\Controller { $o = ''; nav_set_selected('Mail'); - + if(! local_channel()) { notice( t('Permission denied.') . EOL); return login(); diff --git a/Zotlabs/Module/Sse.php b/Zotlabs/Module/Sse.php new file mode 100644 index 000000000..97fee5f49 --- /dev/null +++ b/Zotlabs/Module/Sse.php @@ -0,0 +1,108 @@ +<?php + +namespace Zotlabs\Module; + +use App; +use Zotlabs\Lib\Apps; +use Zotlabs\Web\Controller; +use Zotlabs\Lib\Enotify; + +class Sse extends Controller { + + public static $uid; + public static $ob_hash; + public static $vnotify; + + function init() { + + // this is important! + session_write_close(); + + $sys = get_sys_channel(); + + self::$uid = local_channel(); + self::$ob_hash = get_observer_hash(); + self::$vnotify = get_pconfig(self::$uid, 'system', 'vnotify'); + + $sleep_seconds = 3; + + header("Content-Type: text/event-stream"); + header("Cache-Control: no-cache"); + header("Connection: keep-alive"); + header("X-Accel-Buffering: no"); + + while(true) { + + /** + * Update chat presence indication (if applicable) + */ + + if(self::$ob_hash) { + $r = q("select cp_id, cp_room from chatpresence where cp_xchan = '%s' and cp_client = '%s' and cp_room = 0 limit 1", + dbesc(self::$ob_hash), + dbesc($_SERVER['REMOTE_ADDR']) + ); + $basic_presence = false; + if($r) { + $basic_presence = true; + q("update chatpresence set cp_last = '%s' where cp_id = %d", + dbesc(datetime_convert()), + intval($r[0]['cp_id']) + ); + } + if(! $basic_presence) { + q("insert into chatpresence ( cp_xchan, cp_last, cp_status, cp_client) + values( '%s', '%s', '%s', '%s' ) ", + dbesc(self::$ob_hash), + dbesc(datetime_convert()), + dbesc('online'), + dbesc($_SERVER['REMOTE_ADDR']) + ); + } + } + + /** + * Chatpresence continued... if somebody hasn't pinged recently, they've most likely left the page + * and shouldn't count as online anymore. We allow an expection for bots. + */ + q("delete from chatpresence where cp_last < %s - INTERVAL %s and cp_client != 'auto' ", + db_utcnow(), + db_quoteinterval('3 MINUTE') + ); + + $x = q("SELECT v FROM xconfig WHERE xchan = '%s' AND cat = 'sse' AND k = 'notifications'", + dbesc(self::$ob_hash) + ); + + if($x) { + $result = unserialize($x[0]['v']); + } + + if($result) { + echo "event: notifications\n"; + echo 'data: ' . json_encode($result); + echo "\n\n"; + + del_xconfig(self::$ob_hash, 'sse', 'notifications'); + unset($result); + } + + // always send heartbeat to detect disconnected clients + echo "event: heartbeat\n"; + echo 'data: {}'; + echo "\n\n"; + + ob_end_flush(); + flush(); + + if(connection_status() != CONNECTION_NORMAL || connection_aborted()) { + break; + } + + sleep($sleep_seconds); + + } + + } + +} diff --git a/Zotlabs/Module/Sse_bs.php b/Zotlabs/Module/Sse_bs.php new file mode 100644 index 000000000..672a6c5ef --- /dev/null +++ b/Zotlabs/Module/Sse_bs.php @@ -0,0 +1,519 @@ +<?php + +namespace Zotlabs\Module; + +use App; +use Zotlabs\Lib\Apps; +use Zotlabs\Web\Controller; +use Zotlabs\Lib\Enotify; + +class Sse_bs extends Controller { + + public static $uid; + public static $ob_hash; + public static $vnotify; + public static $evdays; + public static $limit; + public static $offset; + public static $xchans; + + function init() { + + self::$uid = local_channel(); + self::$ob_hash = get_observer_hash(); + self::$vnotify = get_pconfig(self::$uid, 'system', 'vnotify'); + self::$evdays = intval(get_pconfig(self::$uid, 'system', 'evdays')); + self::$limit = 100; + self::$offset = 0; + self::$xchans = ''; + + if(!empty($_GET['nquery']) && $_GET['nquery'] !== '%') { + $nquery = $_GET['nquery']; + + $x = q("SELECT xchan_hash FROM xchan WHERE xchan_name LIKE '%s' OR xchan_addr LIKE '%s'", + dbesc($nquery . '%'), + dbesc($nquery . '%') + ); + + self::$xchans = ids_to_querystr($x, 'xchan_hash', true); + } + + if(intval(argv(2)) > 0) + self::$offset = argv(2); + else + $_SESSION['sse_loadtime'] = datetime_convert(); + + $network = false; + $home = false; + $pubs = false; + $f = ''; + + switch (argv(1)) { + case 'network': + $network = true; + $f = 'bs_network'; + break; + case 'home': + $home = true; + $f = 'bs_home'; + break; + case 'pubs': + $pubs = true; + $f = 'bs_pubs'; + break; + default: + } + + //hz_syslog('init: ' . argv(1)); + //hz_syslog('offset: ' . argv(2)); + + if(self::$offset && $f) { + $result = self::$f(true); + json_return_and_die($result); + } + + $result = array_merge( + self::bs_network($network), + self::bs_home($home), + self::bs_notify(), + self::bs_intros(), + self::bs_forums(), + self::bs_pubs($pubs), + self::bs_files(), + self::bs_mail(), + self::bs_all_events(), + self::bs_register() + ); + + json_return_and_die($result); + } + + function bs_network($notifications) { + + $result['network']['notifications'] = []; + $result['network']['count'] = 0; + + if(! self::$uid) + return $result; + + $limit = intval(self::$limit); + $offset = self::$offset; + + $sql_extra = ''; + if(! (self::$vnotify & VNOTIFY_LIKE)) + $sql_extra = " AND verb NOT IN ('" . dbesc(ACTIVITY_LIKE) . "', '" . dbesc(ACTIVITY_DISLIKE) . "') "; + + $sql_extra2 = ''; + if(self::$xchans) + $sql_extra2 = " AND (author_xchan IN (" . self::$xchans . ") OR owner_xchan IN (" . self::$xchans . ")) "; + + $item_normal = item_normal(); + + if ($notifications) { + $items = q("SELECT * FROM item + WHERE uid = %d + AND created <= '%s' + AND item_unseen = 1 AND item_wall = 0 + AND author_xchan != '%s' + $item_normal + $sql_extra + $sql_extra2 + ORDER BY created DESC LIMIT $limit OFFSET $offset", + intval(self::$uid), + dbescdate($_SESSION['sse_loadtime']), + dbesc(self::$ob_hash) + ); + + if($items) { + $result['network']['offset'] = ((count($items) == $limit) ? intval($offset + $limit) : -1); + xchan_query($items); + foreach($items as $item) { + $result['network']['notifications'][] = Enotify::format($item); + } + } + else { + $result['network']['offset'] = -1; + } + + } + + $r = q("SELECT count(id) as total FROM item + WHERE uid = %d and item_unseen = 1 AND item_wall = 0 + $item_normal + $sql_extra + AND author_xchan != '%s'", + intval(self::$uid), + dbesc(self::$ob_hash) + ); + + if($r) + $result['network']['count'] = intval($r[0]['total']); + + return $result; + } + + function bs_home($notifications) { + + $result['home']['notifications'] = []; + $result['home']['count'] = 0; + + if(! self::$uid) + return $result; + + $limit = intval(self::$limit); + $offset = self::$offset; + + $sql_extra = ''; + if(! (self::$vnotify & VNOTIFY_LIKE)) + $sql_extra = " AND verb NOT IN ('" . dbesc(ACTIVITY_LIKE) . "', '" . dbesc(ACTIVITY_DISLIKE) . "') "; + + $sql_extra2 = ''; + if(self::$xchans) + $sql_extra2 = " AND (author_xchan IN (" . self::$xchans . ") OR owner_xchan IN (" . self::$xchans . ")) "; + + + $item_normal = item_normal(); + + if ($notifications) { + $items = q("SELECT * FROM item + WHERE uid = %d + AND created <= '%s' + AND item_unseen = 1 AND item_wall = 1 + AND author_xchan != '%s' + $item_normal + $sql_extra + $sql_extra2 + ORDER BY created DESC LIMIT $limit OFFSET $offset", + intval(self::$uid), + dbescdate($_SESSION['sse_loadtime']), + dbesc(self::$ob_hash) + ); + + if($items) { + $result['home']['offset'] = ((count($items) == $limit) ? intval($offset + $limit) : -1); + xchan_query($items); + foreach($items as $item) { + $result['home']['notifications'][] = Enotify::format($item); + } + } + else { + $result['home']['offset'] = -1; + } + + } + + $r = q("SELECT count(id) as total FROM item + WHERE uid = %d and item_unseen = 1 AND item_wall = 1 + $item_normal + $sql_extra + AND author_xchan != '%s'", + intval(self::$uid), + dbesc(self::$ob_hash) + ); + + if($r) + $result['home']['count'] = intval($r[0]['total']); + + return $result; + } + + function bs_pubs($notifications) { + + $result['pubs']['notifications'] = []; + $result['pubs']['count'] = 0; + + if(! isset($_SESSION['static_loadtime'])) + $_SESSION['static_loadtime'] = datetime_convert(); + + $limit = intval(self::$limit); + $offset = self::$offset; + + $sys = get_sys_channel(); + $sql_extra = ''; + if(! (self::$vnotify & VNOTIFY_LIKE)) + $sql_extra = " AND verb NOT IN ('" . dbesc(ACTIVITY_LIKE) . "', '" . dbesc(ACTIVITY_DISLIKE) . "') "; + + $sql_extra2 = ''; + if(self::$xchans) + $sql_extra2 = " AND (author_xchan IN (" . self::$xchans . ") OR owner_xchan IN (" . self::$xchans . ")) "; + + $item_normal = item_normal(); + + if ($notifications) { + $items = q("SELECT * FROM item + WHERE uid = %d + AND created <= '%s' + AND item_unseen = 1 + AND author_xchan != '%s' + AND created > '%s' + $item_normal + $sql_extra + $sql_extra2 + ORDER BY created DESC LIMIT $limit OFFSET $offset", + intval($sys['channel_id']), + dbescdate($_SESSION['sse_loadtime']), + dbesc(self::$ob_hash), + dbescdate($_SESSION['static_loadtime']) + ); + + if($items) { + $result['pubs']['offset'] = ((count($items) == $limit) ? intval($offset + $limit) : -1); + xchan_query($items); + foreach($items as $item) { + $result['pubs']['notifications'][] = Enotify::format($item); + } + } + else { + $result['pubs']['offset'] = -1; + } + + + } + + $r = q("SELECT count(id) as total FROM item + WHERE uid = %d AND item_unseen = 1 + AND created > '%s' + $item_normal + $sql_extra + AND author_xchan != '%s'", + intval($sys['channel_id']), + dbescdate($_SESSION['static_loadtime']), + dbesc(self::$ob_hash) + ); + + if($r) + $result['pubs']['count'] = intval($r[0]['total']); + + return $result; + } + + + function bs_notify() { + + $result['notify']['notifications'] = []; + $result['notify']['count'] = 0; + $result['notify']['offset'] = -1; + + if(! self::$uid) + return $result; + + $r = q("SELECT * FROM notify WHERE uid = %d AND seen = 0 ORDER BY created DESC", + intval(self::$uid) + ); + if($r) { + foreach($r as $rr) { + $result['notify']['notifications'][] = Enotify::format_notify($rr); + } + $result['notify']['count'] = count($r); + } + + return $result; + + } + + function bs_intros() { + + $result['intros']['notifications'] = []; + $result['intros']['count'] = 0; + $result['intros']['offset'] = -1; + + if(! self::$uid) + return $result; + + $r = q("SELECT * FROM abook left join xchan on abook.abook_xchan = xchan.xchan_hash where abook_channel = %d and abook_pending = 1 and abook_self = 0 and abook_ignored = 0 and xchan_deleted = 0 and xchan_orphan = 0 ORDER BY abook_created DESC LIMIT 50", + intval(self::$uid) + ); + + if($r) { + foreach($r as $rr) { + $result['intros']['notifications'][] = Enotify::format_intros($rr); + } + $result['intros']['count'] = count($r); + } + + return $result; + + } + + function bs_forums() { + + $result['forums']['notifications'] = []; + $result['forums']['count'] = 0; + $result['forums']['offset'] = -1; + + if(! self::$uid) + return $result; + + $forums = get_forum_channels(self::$uid); + + if($forums) { + $item_normal = item_normal(); + + $sql_extra = ''; + if(! (self::$vnotify & VNOTIFY_LIKE)) + $sql_extra = " AND verb NOT IN ('" . dbesc(ACTIVITY_LIKE) . "', '" . dbesc(ACTIVITY_DISLIKE) . "') "; + + $fcount = count($forums); + $i = 0; + + for($x = 0; $x < $fcount; $x ++) { + $p = q("SELECT oid AS parent FROM term WHERE uid = %d AND ttype = %d AND term = '%s'", + intval(self::$uid), + intval(TERM_FORUM), + dbesc($forums[$x]['xchan_name']) + ); + + $p_str = ids_to_querystr($p, 'parent'); + $p_sql = (($p_str) ? "OR parent IN ( $p_str )" : ''); + + $r = q("select count(id) as unseen from item + where uid = %d and ( owner_xchan = '%s' OR author_xchan = '%s' $p_sql ) and item_unseen = 1 $sql_extra $item_normal", + intval(self::$uid), + dbesc($forums[$x]['xchan_hash']), + dbesc($forums[$x]['xchan_hash']) + ); + + if($r[0]['unseen']) { + $forums[$x]['notify_link'] = (($forums[$x]['private_forum']) ? $forums[$x]['xchan_url'] : z_root() . '/network/?f=&pf=1&unseen=1&cid=' . $forums[$x]['abook_id']); + $forums[$x]['name'] = $forums[$x]['xchan_name']; + $forums[$x]['addr'] = $forums[$x]['xchan_addr']; + $forums[$x]['url'] = $forums[$x]['xchan_url']; + $forums[$x]['photo'] = $forums[$x]['xchan_photo_s']; + $forums[$x]['unseen'] = $r[0]['unseen']; + $forums[$x]['private_forum'] = (($forums[$x]['private_forum']) ? 'lock' : ''); + $forums[$x]['message'] = (($forums[$x]['private_forum']) ? t('Private forum') : t('Public forum')); + + unset($forums[$x]['abook_id']); + unset($forums[$x]['xchan_hash']); + unset($forums[$x]['xchan_name']); + unset($forums[$x]['xchan_url']); + unset($forums[$x]['xchan_photo_s']); + + $i = $i + $r[0]['unseen']; + + } + else { + unset($forums[$x]); + } + } + + $result['forums']['count'] = $i; + $result['forums']['notifications'] = array_values($forums); + + } + + return $result; + + } + + function bs_files() { + + $result['files']['notifications'] = []; + $result['files']['count'] = 0; + $result['files']['offset'] = -1; + + if(! self::$uid) + return $result; + + $r = q("SELECT * FROM item + WHERE verb = '%s' + AND obj_type = '%s' + AND uid = %d + AND owner_xchan != '%s' + AND item_unseen = 1", + dbesc(ACTIVITY_POST), + dbesc(ACTIVITY_OBJ_FILE), + intval(self::$uid), + dbesc(self::$ob_hash) + ); + if($r) { + xchan_query($r); + foreach($r as $rr) { + $result['files']['notifications'][] = Enotify::format_files($rr); + } + $result['files']['count'] = count($r); + } + + return $result; + + } + + function bs_mail() { + + $result['mail']['notifications'] = []; + $result['mail']['count'] = 0; + $result['mail']['offset'] = -1; + + if(! self::$uid) + return $result; + + $r = q("select mail.*, xchan.* from mail left join xchan on xchan_hash = from_xchan + where channel_id = %d and mail_seen = 0 and mail_deleted = 0 + and from_xchan != '%s' order by created desc", + intval(self::$uid), + dbesc(self::$ob_hash) + ); + + if($r) { + foreach($r as $rr) { + $result['mail']['notifications'][] = Enotify::format_mail($rr); + } + $result['mail']['count'] = count($r); + } + + return $result; + + } + + function bs_all_events() { + + $result['all_events']['notifications'] = []; + $result['all_events']['count'] = 0; + $result['all_events']['offset'] = -1; + + if(! self::$uid) + return $result; + + $r = q("SELECT * FROM event left join xchan on event_xchan = xchan_hash + WHERE event.uid = %d AND dtstart < '%s' AND dtstart > '%s' and dismissed = 0 + and etype in ( 'event', 'birthday' ) + ORDER BY dtstart DESC", + intval(self::$uid), + dbesc(datetime_convert('UTC', date_default_timezone_get(), 'now + ' . intval(self::$evdays) . ' days')), + dbesc(datetime_convert('UTC', date_default_timezone_get(), 'now - 1 days')) + ); + + if($r) { + foreach($r as $rr) { + $result['all_events']['notifications'][] = Enotify::format_all_events($rr); + } + $result['all_events']['count'] = count($r); + } + + return $result; + } + + function bs_register() { + + $result['register']['notifications'] = []; + $result['register']['count'] = 0; + $result['register']['offset'] = -1; + + if(! self::$uid && ! is_site_admin()) + return $result; + + $r = q("SELECT account_email, account_created from account where (account_flags & %d) > 0", + intval(ACCOUNT_PENDING) + ); + if($r) { + foreach($r as $rr) { + $result['register']['notifications'][] = Enotify::format_register($rr); + } + $result['register']['count'] = count($r); + } + + return $result; + + } + +} @@ -1811,6 +1811,8 @@ function can_view_public_stream() { * @param string $s Text to display */ function notice($s) { + +/* if(! session_id()) return; @@ -1826,6 +1828,34 @@ function notice($s) { if(App::$interactive) { $_SESSION['sysmsg'][] = $s; } +*/ + + $hash = get_observer_hash(); + + if (! $hash) + return; + + + $t = get_xconfig($hash, 'sse', 'timestamp'); + + if(datetime_convert('UTC', 'UTC', $t) < datetime_convert('UTC', 'UTC', '- 30 seconds')) { + del_xconfig($hash, 'sse', 'notifications'); + } + + $x = get_xconfig($hash, 'sse', 'notifications'); + + if ($x === false) + $x = []; + + if (isset($x['notice']) && in_array($s, $x['notice']['notifications'])) + return; + + if (App::$interactive) { + $x['notice']['notifications'][] = $s; + set_xconfig($hash, 'sse', 'timestamp', datetime_convert()); + set_xconfig($hash, 'sse', 'notifications', $x); + } + } /** @@ -1839,8 +1869,11 @@ function notice($s) { * @param string $s Text to display */ function info($s) { + +/* if(! session_id()) return; + if(! x($_SESSION, 'sysmsg_info')) $_SESSION['sysmsg_info'] = array(); @@ -1849,6 +1882,33 @@ function info($s) { if(App::$interactive) $_SESSION['sysmsg_info'][] = $s; +*/ + + $hash = get_observer_hash(); + + if (! $hash) + return; + + $t = get_xconfig($hash, 'sse', 'timestamp'); + + if(datetime_convert('UTC', 'UTC', $t) < datetime_convert('UTC', 'UTC', '- 30 seconds')) { + del_xconfig($hash, 'sse', 'notifications'); + } + + $x = get_xconfig($hash, 'sse', 'notifications'); + + if($x === false) + $x = []; + + if(isset($x['info']) && in_array($s, $x['info']['notifications'])) + return; + + if(App::$interactive) { + $x['info']['notifications'][] = $s; + set_xconfig($hash, 'sse', 'timestamp', datetime_convert()); + set_xconfig($hash, 'sse', 'notifications', $x); + } + } /** diff --git a/doc/hooklist.bb b/doc/hooklist.bb index a923e7ae3..4774b9978 100644 --- a/doc/hooklist.bb +++ b/doc/hooklist.bb @@ -259,6 +259,9 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/enotify_store]enotify_store[/zrl] called when storing a notification record +[zrl=[baseurl]/help/hook/enotify_store_end]enotify_store_end[/zrl] + called after a notification record has been stored + [zrl=[baseurl]/help/hook/event_created]event_created[/zrl] called when an event record is created diff --git a/include/channel.php b/include/channel.php index 29835eac6..32bd596fc 100644 --- a/include/channel.php +++ b/include/channel.php @@ -2953,3 +2953,8 @@ function pchan_to_chan($pchan) { function channel_url($channel) { return (($channel) ? z_root() . '/channel/' . $channel['channel_address'] : z_root()); } + +function get_channel_hashes() { + $r = q("SELECT channel_hash FROM channel WHERE channel_removed = 0"); + return flatten_array_recursive($r); +} diff --git a/include/event.php b/include/event.php index 6be1b6705..9d76aabd6 100644 --- a/include/event.php +++ b/include/event.php @@ -553,9 +553,19 @@ function event_store_event($arr) { dbesc($hash), intval($arr['uid']) ); - if($r) + if($r) { + + /** + * @hooks event_store_event_end + * Called after an event record was stored. + * * \e array \b event + */ + call_hooks('event_store_event_end', $r[0]); + return $r[0]; + } + return false; } diff --git a/view/css/widgets.css b/view/css/widgets.css index 995647d1c..ca7267189 100644 --- a/view/css/widgets.css +++ b/view/css/widgets.css @@ -181,7 +181,6 @@ a.wikilist { .notifications-textinput { padding: .75rem 0.85rem; - position: relative; } .notifications-textinput input { @@ -233,3 +232,7 @@ a.wikilist { margin-top: 25px; opacity: 0.8; } + +#cid-filter-wrapper { + position: relative; +} diff --git a/view/de-de/hstrings.php b/view/de-de/hstrings.php index 5e16040e2..2f411a1d1 100644 --- a/view/de-de/hstrings.php +++ b/view/de-de/hstrings.php @@ -77,7 +77,7 @@ App::$strings["Search"] = "Suche"; App::$strings["Search site @name, !forum, #tag, ?docs, content"] = "Hub durchsuchen: @Name, !Forum, #Schlagwort, ?Dokumentation, Inhalt"; App::$strings["Admin"] = "Administration"; App::$strings["Site Setup and Configuration"] = "Seiten-Einrichtung und -Konfiguration"; -App::$strings["Loading"] = "Lädt..."; +App::$strings["Loading"] = "Lädt"; App::$strings["@name, !forum, #tag, ?doc, content"] = "@Name, !Forum, #Schlagwort, ?Dokumentation, Inhalt"; App::$strings["Please wait..."] = "Bitte warten..."; App::$strings["Add Apps"] = "Apps hinzufügen"; diff --git a/view/js/main.js b/view/js/main.js index f3b8151b0..f15636a35 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -24,7 +24,31 @@ var contentHeightDiff = 0; var liveRecurse = 0; var savedTitle = ''; var initialLoad = true; +var window_needs_alert = true; + +var sse_bs_active = false; +var sse_offset = 0; +var sse_type; +var sse_partial_result = false; + +// take care of tab/window reloads on channel change +if(localStorage.getItem('uid') !== localUser.toString()) { + localStorage.setItem('uid', localUser.toString()); +} +window.onstorage = function(e) { + if(e.key === 'uid' && parseInt(e.newValue) !== localUser) { + if(window_needs_alert) { + window_needs_alert = false; + localStorage.clear(); + sessionStorage.clear(); + alert("Your identity has changed. Page reload required!"); + window.location.reload(); + return; + } + } +} +/* // Clear the session and local storage if we switch channel or log out var cache_uid = ''; if(sessionStorage.getItem('uid') !== null) { @@ -35,6 +59,7 @@ if(cache_uid !== localUser.toString()) { localStorage.clear(); sessionStorage.setItem('uid', localUser.toString()); } +*/ $.ajaxSetup({cache: false}); @@ -55,28 +80,114 @@ $(document).ready(function() { } }); - var tf = new Function('n', 's', 'var k = s.split("/")['+aStr['plural_func']+']; return (k ? k : s);'); - - jQuery.timeago.settings.strings = { - prefixAgo : aStr['t01'], - prefixFromNow : aStr['t02'], - suffixAgo : aStr['t03'], - suffixFromNow : aStr['t04'], - seconds : aStr['t05'], - minute : aStr['t06'], - minutes : function(value){return tf(value, aStr['t07']);}, - hour : aStr['t08'], - hours : function(value){return tf(value, aStr['t09']);}, - day : aStr['t10'], - days : function(value){return tf(value, aStr['t11']);}, - month : aStr['t12'], - months : function(value){return tf(value, aStr['t13']);}, - year : aStr['t14'], - years : function(value){return tf(value, aStr['t15']);}, - wordSeparator : aStr['t16'], - numbers : aStr['t17'], - }; + var tf = new Function('n', 's', 'var k = s.split("/")['+aStr['plural_func']+']; return (k ? k : s);'); + + jQuery.timeago.settings.strings = { + prefixAgo : aStr['t01'], + prefixFromNow : aStr['t02'], + suffixAgo : aStr['t03'], + suffixFromNow : aStr['t04'], + seconds : aStr['t05'], + minute : aStr['t06'], + minutes : function(value){return tf(value, aStr['t07']);}, + hour : aStr['t08'], + hours : function(value){return tf(value, aStr['t09']);}, + day : aStr['t10'], + days : function(value){return tf(value, aStr['t11']);}, + month : aStr['t12'], + months : function(value){return tf(value, aStr['t13']);}, + year : aStr['t14'], + years : function(value){return tf(value, aStr['t15']);}, + wordSeparator : aStr['t16'], + numbers : aStr['t17'], + }; + + + if(typeof(window.SharedWorker) === 'undefined') { + // notifications with multiple tabs open will not work very well in this scenario + var evtSource = new EventSource('/sse'); + + evtSource.addEventListener('notifications', function(e) { + var obj = JSON.parse(e.data); + sse_handleNotifications(obj, false, false); + }, false); + + document.addEventListener('visibilitychange', function() { + if (!document.hidden) { + sse_offset = 0; + sse_bs_init(); + } + }, false); + + } + else { + var myWorker = new SharedWorker('/view/js/sse_worker.js', localUser); + + myWorker.port.onmessage = function(e) { + obj = e.data; + console.log(obj); + sse_handleNotifications(obj, false, false); + } + + myWorker.onerror = function(e) { + myWorker.port.close(); + } + + myWorker.port.start(); + } + + sse_bs_init(); + + $('.notification-link').on('click', { replace: true, followup: false }, sse_bs_notifications); + + $('.notification-filter').on('keypress', function(e) { + if(e.which == 13) { // enter + this.blur(); + sse_offset = 0; + $("#nav-" + sse_type + "-menu").html(''); + $("#nav-" + sse_type + "-loading").show(); + + var cn_val = $('#cn-' + sse_type + '-input').length ? $('#cn-' + sse_type + '-input').val().toString().toLowerCase() : ''; + + $.get('/sse_bs/' + sse_type + '/' + sse_offset + '?nquery=' + encodeURIComponent(cn_val), function(obj) { + console.log('sse: bootstraping ' + sse_type); + console.log(obj); + sse_handleNotifications(obj, true, false); + sse_bs_active = false; + sse_partial_result = true; + sse_offset = obj[sse_type].offset; + if(sse_offset < 0) + $("#nav-" + sse_type + "-loading").hide(); + + }); + } + }); + + $('.notifications-textinput-clear').on('click', function(e) { + if(! sse_partial_result) + return; + + $("#nav-" + sse_type + "-menu").html(''); + $("#nav-" + sse_type + "-loading").show(); + $.get('/sse_bs/' + sse_type, function(obj) { + console.log('sse: bootstraping ' + sse_type); + console.log(obj); + sse_handleNotifications(obj, true, false); + sse_bs_active = false; + sse_partial_result = false; + sse_offset = obj[sse_type].offset; + if(sse_offset < 0) + $("#nav-" + sse_type + "-loading").hide(); + + }); + }); + $('.notification-content').on('scroll', function() { + if(this.scrollTop > this.scrollHeight - this.clientHeight - (this.scrollHeight/7)) { + if(!sse_bs_active) + sse_bs_notifications(sse_type, false, true); + } + }); //mod_mail only $(".mail-conv-detail .autotime").timeago(); @@ -85,6 +196,7 @@ $(document).ready(function() { updateInit(); +/* $('a.notification-link').click(function(e){ var notifyType = $(this).data('type'); @@ -102,6 +214,7 @@ $(document).ready(function() { $('#nav-' + notifyType + '-sub').addClass('show'); loadNotificationItems(notifyType); } +*/ // Allow folks to stop the ajax page updates with the pause/break key $(document).keydown(function(event) { @@ -462,6 +575,7 @@ function markItemRead(itemId) { $('.unseen-wall-indicator-'+itemId).hide(); } +/* function notificationsUpdate(cached_data) { var pingCmd = 'ping' + ((localUser != 0) ? '?f=&uid=' + localUser : ''); @@ -545,7 +659,7 @@ function handleNotifications(data) { $.each(data, function(index, item) { //do not process those - var arr = ['notice', 'info', 'invalid']; + var arr = ['notice', 'info', 'invalid', 'network', 'home', 'notify', 'intros']; if(arr.indexOf(index) !== -1) return; @@ -565,10 +679,11 @@ function handleNotificationsItems(notifyType, data) { notify_menu.html(''); - $(data).each(function() { + $(data.reverse()).each(function() { html = notifications_tpl.format(this.notify_link,this.photo,this.name,this.addr,this.message,this.when,this.hclass,this.b64mid,this.notify_id,this.thread_top,this.unseen,this.private_forum); - notify_menu.append(html); + notify_menu.prepend(html); }); + $(".notifications-autotime").timeago(); datasrc2src('#notifications .notification img[data-src]'); @@ -578,6 +693,7 @@ function handleNotificationsItems(notifyType, data) { if($('#cn-' + notifyType + '-input').length) { var filter = $('#cn-' + notifyType + '-input').val().toString().toLowerCase(); if(filter) { + filter = filter.indexOf('%') == 0 ? filter.substring(1) : filter; $('#nav-' + notifyType + '-menu .notification').each(function(i, el){ var cn = $(el).data('contact_name').toString().toLowerCase(); var ca = $(el).data('contact_addr').toString().toLowerCase(); @@ -589,6 +705,7 @@ function handleNotificationsItems(notifyType, data) { } } } +*/ function contextualHelp() { var container = $("#contextual-help-content"); @@ -718,13 +835,10 @@ function updateConvItems(mode,data) { } } - - // trigger the autotime function on all newly created content - $("> .wall-item-outside-wrapper .autotime, > .thread-wrapper .autotime",this).timeago(); $("> .shared_header .autotime",this).timeago(); - + if((mode === 'append' || mode === 'replace') && (loadingPage)) { loadingPage = false; } @@ -744,6 +858,42 @@ function updateConvItems(mode,data) { } }); + + // take care of the notifications count updates + $('.thread-wrapper', data).each(function() { + + var nmid = $(this).data('b64mid'); + + if($('.notification[data-b64mid=\'' + nmid + '\']').length) { + $('.notification[data-b64mid=\'' + nmid + '\']').each(function() { + var n = this.parentElement.id.split('-'); + + if(n[1] === 'pubs') + return true; + + if(n[1] === 'notify' && (nmid !== bParam_mid || sse_type !== 'notify')) + return true; + + var count = Number($('.' + n[1] + '-update').html()); + + if(count > 0) + count = count - 1; + + if(count < 1) { + $('.' + n[1] + '-button').fadeOut(); + $('.' + n[1] + '-update').html(count); + } + else + $('.' + n[1] + '-update').html(count); + + $('#nav-' + n[1] + '-menu .notification[data-b64mid=\'' + nmid + '\']').fadeOut(); + + }); + } + + }); + + // reset rotators and cursors we may have set before reaching this place $('.like-rotator').hide(); @@ -802,18 +952,15 @@ function updateConvItems(mode,data) { function scrollToItem() { // auto-scroll to a particular comment in a thread (designated by mid) when in single-thread mode - // use the same method to generate the submid as we use in ThreadItem, - // base64_encode + replace(['+','='],['','']); if(justifiedGalleryActive) return; var submid = ((bParam_mid.length) ? bParam_mid : 'abcdefg'); var encoded = ((submid.substr(0,4) == 'b64.') ? true : false); - var submid_encoded = ((encoded) ? submid.substr(4) : window.btoa(submid)); + var submid_encoded = ((encoded) ? submid : window.btoa(submid)); - submid_encoded = submid_encoded.replace(/[\+\=]/g,''); - if($('.item_' + submid_encoded).length && !$('.item_' + submid_encoded).hasClass('toplevel_item')) { + if($('.thread-wrapper[data-b64mid=\'' + submid_encoded + '\']').length && !$('.thread-wrapper[data-b64mid=\'' + submid_encoded + '\']').hasClass('toplevel_item')) { if($('.collapsed-comments').length) { var scrolltoid = $('.collapsed-comments').attr('id').substring(19); $('#collapsed-comments-' + scrolltoid + ' .autotime').timeago(); @@ -821,8 +968,8 @@ function scrollToItem() { $('#hide-comments-' + scrolltoid).html(aStr.showfewer); $('#hide-comments-total-' + scrolltoid).hide(); } - $('html, body').animate({ scrollTop: $('.item_' + submid_encoded).offset().top - $('nav').outerHeight(true) }, 'slow'); - $('.item_' + submid_encoded).addClass('item-highlight'); + $('html, body').animate({ scrollTop: $('.thread-wrapper[data-b64mid=\'' + submid_encoded + '\']').offset().top - $('nav').outerHeight(true) }, 'slow'); + $('.thread-wrapper[data-b64mid=\'' + submid_encoded + '\']').addClass('item-highlight'); } } @@ -885,6 +1032,7 @@ function updateInit() { // if($('#live-cards').length) { src = 'cards'; } // if($('#live-articles').length) { src = 'articles'; } +/* if (initialLoad && (sessionStorage.getItem('notifications_cache') !== null)) { var cached_data = JSON.parse(sessionStorage.getItem('notifications_cache')); notificationsUpdate(cached_data); @@ -898,9 +1046,10 @@ function updateInit() { } } +*/ if(! src) { - notificationsUpdate(); + // notificationsUpdate(); } else { liveUpdate(); @@ -1039,7 +1188,7 @@ function liveUpdate(notify_id) { }) .done(function() { - notificationsUpdate(); + // notificationsUpdate(); }); } @@ -1088,7 +1237,9 @@ function justifyPhotosAjax(id) { $('#' + id).justifiedGallery('norewind').on('jg.complete', function(e){ justifiedGalleryActive = false; }); } +/* function loadNotificationItems(notifyType) { + var pingExCmd = 'ping/' + notifyType + ((localUser != 0) ? '?f=&uid=' + localUser : ''); var clicked = $('[data-type=\'' + notifyType + '\']').data('clicked'); @@ -1119,6 +1270,7 @@ function loadNotificationItems(notifyType) { } }); } +*/ // Since our ajax calls are asynchronous, we will give a few // seconds for the first ajax call (setting like/dislike), then @@ -1169,7 +1321,7 @@ function doscroll(parent, hidden) { } } back.remove(); - var id = $('[data-mid="' + parent + '"]'); + var id = $('[data-b64mid="' + parent + '"]'); $('html, body').animate({scrollTop:(id.offset().top) - 50}, 'slow'); $('<a href="javascript:doscrollback(' + pos + ');" id="back-to-reply" class="float-right" title="' + aStr['to_reply'] + '"><i class="fa fa-angle-double-down"> </i></a>').insertBefore('#wall-item-info-' + id.attr('id').replace(/\D/g,'')); } @@ -1653,3 +1805,306 @@ function zid(s) { return s; } + +function sse_bs_init() { + if(sessionStorage.getItem('notification_open') !== null || typeof sse_type !== 'undefined' ) { + if(typeof sse_type === 'undefined') + sse_type = sessionStorage.getItem('notification_open'); + + $("#nav-" + sse_type + "-sub").addClass('show'); + sse_bs_notifications(sse_type, true, false); + } + else { + $.get('/sse_bs',function(obj) { + console.log(obj); + sse_handleNotifications(obj, true, false); + }); + } +} + +function sse_bs_notifications(e, replace, followup) { + + sse_bs_active = true; + var manual = false; + + if(typeof replace === 'undefined') + replace = e.data.replace; + + if(typeof followup === 'undefined') + followup = e.data.followup; + + if(typeof e === 'string') { + sse_type = e; + } + else { + manual = true; + sse_offset = 0; + sse_type = e.target.dataset.sse_type; + } + + if(typeof sse_type === 'undefined') + return; + + if(followup || !manual || !($('#nav-' + sse_type + '-sub').hasClass('collapse') && $('#nav-' + sse_type + '-sub').hasClass('show'))) { + + if(sse_offset >= 0) { + $("#nav-" + sse_type + "-loading").show(); + } + + sessionStorage.setItem('notification_open', sse_type); + if(sse_offset !== -1 || replace) { + + var cn_val = (($('#cn-' + sse_type + '-input').length && sse_partial_result) ? $('#cn-' + sse_type + '-input').val().toString().toLowerCase() : ''); + + $.get('/sse_bs/' + sse_type + '/' + sse_offset + '?nquery=' + encodeURIComponent(cn_val), function(obj) { + console.log('sse: bootstraping ' + sse_type); + console.log(obj); + sse_handleNotifications(obj, replace, followup); + sse_bs_active = false; + sse_offset = obj[sse_type].offset; + if(sse_offset < 0) + $("#nav-" + sse_type + "-loading").hide(); + + }); + } + else + $("#nav-" + sse_type + "-loading").hide(); + + } + else { + sessionStorage.removeItem('notification_open'); + } +} + + + +function sse_handleNotifications(obj, replace, followup) { + + if( + (obj.network && obj.network.count) || + (obj.home && obj.home.count) || + (obj.intros && obj.intros.count) || + (obj.register && obj.register.count) || + (obj.mail && obj.mail.count) || + (obj.all_events && obj.all_events.count) || + (obj.notify && obj.notify.count) || + (obj.files && obj.files.count) || + (obj.pubs && obj.pubs.count) || + (obj.forums && obj.forums.count) + ) { + $('.notifications-btn').css('opacity', 1); + $('#no_notifications').hide(); + } + else { + $('.notifications-btn').css('opacity', 0.5); + $('#navbar-collapse-1').removeClass('show'); + $('#no_notifications').show(); + } + + if( + (obj.home && obj.home.count) || + (obj.intros && obj.intros.count) || + (obj.register && obj.register.count) || + (obj.mail && obj.mail.count) || + (obj.notify && obj.notify.count) || + (obj.files && obj.files.count) + ) { + $('.notifications-btn-icon').removeClass('fa-exclamation-circle'); + $('.notifications-btn-icon').addClass('fa-exclamation-triangle'); + } + if( + !(obj.home && obj.home.count) && + !(obj.intros && obj.intros.count) && + !(obj.register && obj.register.count) && + !(obj.mail && obj.mail.count) && + !(obj.notify && obj.notify.count) && + !(obj.files && obj.files.count) + ) { + $('.notifications-btn-icon').removeClass('fa-exclamation-triangle'); + $('.notifications-btn-icon').addClass('fa-exclamation-circle'); + } + if(obj.all_events_today && obj.all_events_today.count) { + $('.all_events-update').removeClass('badge-secondary'); + $('.all_events-update').addClass('badge-danger'); + } + else { + $('.all_events-update').removeClass('badge-danger'); + $('.all_events-update').addClass('badge-secondary'); + } + + // network + if(obj.network && obj.network.count) { + $('.network-button').fadeIn(); + if(replace || followup) + $('.network-update').html(Number(obj.network.count)); + else + $('.network-update').html(Number(obj.network.count) + Number($('.network-update').html())); + } + if(obj.network && obj.network.notifications.length) + sse_handleNotificationsItems('network', obj.network.notifications, replace, followup); + + // home + if(obj.home && obj.home.count) { + $('.home-button').fadeIn(); + if(replace || followup) + $('.home-update').html(Number(obj.home.count)); + else + $('.home-update').html(Number(obj.home.count) + Number($('.home-update').html())); + } + if(obj.home && obj.home.notifications.length) + sse_handleNotificationsItems('home', obj.home.notifications, replace, followup); + + // notify + if(obj.notify && obj.notify.count) { + $('.notify-button').fadeIn(); + if(replace || followup) + $('.notify-update').html(Number(obj.notify.count)); + else + $('.notify-update').html(Number(obj.notify.count) + Number($('.notify-update').html())); + + } + if(obj.notify && obj.notify.notifications.length) + sse_handleNotificationsItems('notify', obj.notify.notifications, replace, false); + + // intros + if(obj.intros && obj.intros.count) { + $('.intros-button').fadeIn(); + if(replace || followup) + $('.intros-update').html(Number(obj.intros.count)); + else + $('.intros-update').html(Number(obj.intros.count) + Number($('.intros-update').html())); + } + if(obj.intros && obj.intros.notifications.length) + sse_handleNotificationsItems('intros', obj.intros.notifications, replace, false); + + // forums + if(obj.forums && obj.forums.count) { + $('.forums-button').fadeIn(); + if(replace || followup) + $('.forums-update').html(Number(obj.forums.count)); + else + $('.forums-update').html(Number(obj.forums.count) + Number($('.forums-update').html())); + } + if(obj.forums && obj.forums.notifications.length) + sse_handleNotificationsItems('forums', obj.forums.notifications, replace, false); + + // pubs + if(obj.pubs && obj.pubs.count) { + $('.pubs-button').fadeIn(); + if(replace || followup) + $('.pubs-update').html(Number(obj.pubs.count)); + else + $('.pubs-update').html(Number(obj.pubs.count) + Number($('.pubs-update').html())); + } + if(obj.pubs && obj.pubs.notifications.length) + sse_handleNotificationsItems('pubs', obj.pubs.notifications, replace, followup); + + // files + if(obj.files && obj.files.count) { + $('.files-button').fadeIn(); + if(replace || followup) + $('.files-update').html(Number(obj.files.count)); + else + $('.files-update').html(Number(obj.files.count) + Number($('.files-update').html())); + } + if(obj.files && obj.files.notifications.length) + sse_handleNotificationsItems('files', obj.files.notifications, replace, false); + + // mail + if(obj.mail && obj.mail.count) { + $('.mail-button').fadeIn(); + if(replace || followup) + $('.mail-update').html(Number(obj.mail.count)); + else + $('.mail-update').html(Number(obj.mail.count) + Number($('.mail-update').html())); + } + if(obj.mail && obj.mail.notifications.length) + sse_handleNotificationsItems('mail', obj.mail.notifications, replace, false); + + // all_events + if(obj.all_events && obj.all_events.count) { + $('.all_events-button').fadeIn(); + if(replace || followup) + $('.all_events-update').html(Number(obj.all_events.count)); + else + $('.all_events-update').html(Number(obj.all_events.count) + Number($('.all_events-update').html())); + } + if(obj.all_events && obj.all_events.notifications.length) + sse_handleNotificationsItems('all_events', obj.all_events.notifications, replace, false); + + // register + if(obj.register && obj.register.count) { + $('.register-button').fadeIn(); + if(replace || followup) + $('.register-update').html(Number(obj.register.count)); + else + $('.register-update').html(Number(obj.register.count) + Number($('.register-update').html())); + } + if(obj.register && obj.register.notifications.length) + sse_handleNotificationsItems('register', obj.register.notifications, replace, false); + + // notice and info + $.jGrowl.defaults.closerTemplate = '<div>[ ' + aStr.closeAll + ']</div>'; + if(obj.notice) { + $(obj.notice.notifications).each(function() { + $.jGrowl(this, { sticky: true, theme: 'notice' }); + }); + } + if(obj.info) { + $(obj.info.notifications).each(function(){ + $.jGrowl(this, { sticky: false, theme: 'info', life: 10000 }); + }); + } + +} + +function sse_handleNotificationsItems(notifyType, data, replace, followup) { + console.log('replace: ' + replace); + console.log('followup: ' + followup); + var notifications_tpl = ((notifyType == 'forums') ? unescape($("#nav-notifications-forums-template[rel=template]").html()) : unescape($("#nav-notifications-template[rel=template]").html())); + var notify_menu = $("#nav-" + notifyType + "-menu"); + var notify_loading = $("#nav-" + notifyType + "-loading"); + var notify_count = $("." + notifyType + "-update"); + + if(replace && !followup) { + notify_menu.html(''); + notify_loading.hide(); + } + + $(data).each(function() { + html = notifications_tpl.format(this.notify_link,this.photo,this.name,this.addr,this.message,this.when,this.hclass,this.b64mid,this.notify_id,this.thread_top,this.unseen,this.private_forum); + notify_menu.append(html); + }); + + if(!replace && !followup) { + console.log('sorting'); + $("#nav-" + notifyType + "-menu .notification").sort(function(a,b) { + a = new Date(a.dataset.when); + b = new Date(b.dataset.when); + return a > b ? -1 : a < b ? 1 : 0; + }).appendTo('#nav-' + notifyType + '-menu'); + } + + $(document.body).trigger("sticky_kit:recalc"); + $("#nav-" + notifyType + "-menu .notifications-autotime").timeago(); + + if($('#tt-' + notifyType + '-only').hasClass('active')) + $('#nav-' + notifyType + '-menu [data-thread_top=false]').addClass('d-none'); + + if($('#cn-' + notifyType + '-input').length) { + var filter = $('#cn-' + notifyType + '-input').val().toString().toLowerCase(); + if(filter) { + filter = filter.indexOf('%') == 0 ? filter.substring(1) : filter; + + $('#nav-' + notifyType + '-menu .notification').each(function(i, el) { + var cn = $(el).data('contact_name').toString().toLowerCase(); + var ca = $(el).data('contact_addr').toString().toLowerCase(); + if(cn.indexOf(filter) === -1 && ca.indexOf(filter) === -1) + $(el).addClass('d-none'); + else + $(el).removeClass('d-none'); + }); + } + } +} + diff --git a/view/js/sse_worker.js b/view/js/sse_worker.js new file mode 100755 index 000000000..78e4aa51b --- /dev/null +++ b/view/js/sse_worker.js @@ -0,0 +1,14 @@ +var evtSource = new EventSource('/sse'); + +onconnect = function(e) { + + var port = e.ports[0]; + + port.start(); + + evtSource.addEventListener('notifications', function(e) { + var obj = JSON.parse(e.data); + port.postMessage(obj); + }, false); + +} diff --git a/view/php/theme_init.php b/view/php/theme_init.php index d683a3b58..d47325b77 100644 --- a/view/php/theme_init.php +++ b/view/php/theme_init.php @@ -37,6 +37,7 @@ head_add_js('/library/colorbox/jquery.colorbox-min.js'); head_add_js('/library/jquery.AreYouSure/jquery.are-you-sure.js'); head_add_js('/library/tableofcontents/jquery.toc.js'); head_add_js('/vendor/desandro/imagesloaded/imagesloaded.pkgd.min.js'); + /** * Those who require this feature will know what to do with it. * Those who don't, won't. diff --git a/view/tpl/activity_filter_widget.tpl b/view/tpl/activity_filter_widget.tpl index 7d10100ba..779786828 100644 --- a/view/tpl/activity_filter_widget.tpl +++ b/view/tpl/activity_filter_widget.tpl @@ -9,7 +9,7 @@ </h3> {{$content}} {{if $name}} - <div class="notifications-textinput"> + <div id="cid-filter-wrapper" class="notifications-textinput"> <form method="get" action="{{$name.url}}" role="search"> <div class="text-muted notifications-textinput-filter"><i class="fa fa-fw fa-filter"></i></div> <input id="cid" type="hidden" value="" name="cid" /> diff --git a/view/tpl/conv_item.tpl b/view/tpl/conv_item.tpl index 186551e2d..f639683b8 100755 --- a/view/tpl/conv_item.tpl +++ b/view/tpl/conv_item.tpl @@ -4,9 +4,9 @@ </div> <div id="collapsed-comments-{{$item.id}}" class="collapsed-comments" style="display: none;"> {{/if}} - <div id="thread-wrapper-{{$item.id}}" class="thread-wrapper{{if $item.toplevel}} {{$item.toplevel}} generic-content-wrapper h-entry {{else}} u-comment h-cite {{/if}} item_{{$item.submid}}"> + <div id="thread-wrapper-{{$item.id}}" class="thread-wrapper{{if $item.toplevel}} {{$item.toplevel}} generic-content-wrapper h-entry {{else}} u-comment h-cite{{/if}}" data-b64mid="{{$item.mid}}"> <a name="item_{{$item.id}}" ></a> - <div class="wall-item-outside-wrapper{{if $item.is_comment}} comment{{/if}}{{if $item.previewing}} preview{{/if}}" data-mid="{{$item.mid}}" id="wall-item-outside-wrapper-{{$item.id}}" > + <div class="wall-item-outside-wrapper{{if $item.is_comment}} comment{{/if}}{{if $item.previewing}} preview{{/if}}" id="wall-item-outside-wrapper-{{$item.id}}" > <div class="clearfix wall-item-content-wrapper{{if $item.is_comment}} comment{{/if}}" id="wall-item-content-wrapper-{{$item.id}}"> {{if $item.photo}} <div class="wall-photo-item" id="wall-photo-item-{{$item.id}}"> diff --git a/view/tpl/conv_list.tpl b/view/tpl/conv_list.tpl index 8c5b47bf3..b244311bb 100755 --- a/view/tpl/conv_list.tpl +++ b/view/tpl/conv_list.tpl @@ -4,7 +4,7 @@ </div> <div id="collapsed-comments-{{$item.id}}" class="collapsed-comments" style="display: none;"> {{/if}} - <div id="thread-wrapper-{{$item.id}}" class="thread-wrapper{{if $item.toplevel}} {{$item.toplevel}} generic-content-wrapper h-entry {{else}} u-comment h-cite {{/if}} item_{{$item.submid}}"> + <div id="thread-wrapper-{{$item.id}}" class="thread-wrapper{{if $item.toplevel}} {{$item.toplevel}} generic-content-wrapper h-entry {{else}} u-comment h-cite {{/if}}" data-b64mid="{{$item.mid}}"> <a name="item_{{$item.id}}" ></a> <div class="wall-item-outside-wrapper{{if $item.is_comment}} comment{{/if}}{{if $item.previewing}} preview{{/if}}" id="wall-item-outside-wrapper-{{$item.id}}" > <div class="clearfix wall-item-content-wrapper{{if $item.is_comment}} comment{{/if}}" id="wall-item-content-wrapper-{{$item.id}}"> diff --git a/view/tpl/js_strings.tpl b/view/tpl/js_strings.tpl index 0a9cf9519..440676dee 100755 --- a/view/tpl/js_strings.tpl +++ b/view/tpl/js_strings.tpl @@ -35,7 +35,7 @@ 'name_ok2' : "{{$name_ok2}}", 'to_reply' : "{{$to_reply}}", - 'plural_func' : "{{$plural_func}}", + 'plural_func' : "{{$plural_func}}", 't01' : "{{$t01}}", 't02' : "{{$t02}}", diff --git a/view/tpl/notifications_widget.tpl b/view/tpl/notifications_widget.tpl index bc7f80906..057d5b491 100644 --- a/view/tpl/notifications_widget.tpl +++ b/view/tpl/notifications_widget.tpl @@ -85,7 +85,7 @@ $('#nav-{{$notification.type}}-menu [data-thread_top=false]').toggle(); $(this).toggleClass('active sticky-top'); }); - $(document).on('click ', '#cn-{{$notification.type}}-input-clear', function(e) { + $(document).on('click', '#cn-{{$notification.type}}-input-clear', function(e) { $('#cn-{{$notification.type}}-input').val(''); $('#cn-{{$notification.type}}-only').removeClass('active sticky-top'); $("#nav-{{$notification.type}}-menu .notification").removeClass('d-none'); @@ -93,8 +93,8 @@ }); $(document).on('input', '#cn-{{$notification.type}}-input', function(e) { var val = $('#cn-{{$notification.type}}-input').val().toString().toLowerCase(); - if(val) { + val = val.indexOf('%') == 0 ? val.substring(1) : val; $('#cn-{{$notification.type}}-only').addClass('active sticky-top'); $('#cn-{{$notification.type}}-input-clear').removeClass('d-none'); } @@ -135,16 +135,17 @@ {{$no_notifications}}<span class="jumping-dots"><span class="dot-1">.</span><span class="dot-2">.</span><span class="dot-3">.</span></span> </div> <div id="nav-notifications-template" rel="template"> - <a class="list-group-item clearfix notification {6}" href="{0}" title="{3}" data-b64mid="{7}" data-notify_id="{8}" data-thread_top="{9}" data-contact_name="{2}" data-contact_addr="{3}"> - <img class="menu-img-3" data-src="{1}"> + <a class="list-group-item clearfix notification {6}" href="{0}" title="{3}" data-b64mid="{7}" data-notify_id="{8}" data-thread_top="{9}" data-contact_name="{2}" data-contact_addr="{3}" data-when="{5}"> + <img class="menu-img-3" src="{1}"> <span class="contactname">{2}</span> - <span class="dropdown-sub-text">{4}<br>{5}</span> + <span class="dropdown-sub-text">{4}</span><br> + <span class="dropdown-sub-text notifications-autotime" title="{5}">{5}</span> </a> </div> <div id="nav-notifications-forums-template" rel="template"> <a class="list-group-item clearfix notification notification-forum" href="{0}" title="{4} - {3}" data-b64mid="{7}" data-notify_id="{8}" data-thread_top="{9}" data-contact_name="{2}" data-contact_addr="{3}"> - <span class="float-right badge badge-{{$notification.severity}}">{10}</span> - <img class="menu-img-1" data-src="{1}"> + <span class="float-right badge badge-secondary">{10}</span> + <img class="menu-img-1" src="{1}"> <span class="">{2}</span> <i class="fa fa-{11} text-muted"></i> </a> @@ -152,11 +153,14 @@ <div id="notifications" class="navbar-nav"> {{foreach $notifications as $notification}} <div class="collapse {{$notification.type}}-button"> - <a class="list-group-item notification-link" href="#" title="{{$notification.title}}" data-target="#nav-{{$notification.type}}-sub" data-toggle="collapse" data-type="{{$notification.type}}"> + <a id="notification-link-{{$notification.type}}" class="collapsed list-group-item notification-link" href="#" title="{{$notification.title}}" data-target="#nav-{{$notification.type}}-sub" data-toggle="collapse" data-sse_type="{{$notification.type}}"> <i class="fa fa-fw fa-{{$notification.icon}}"></i> {{$notification.label}} <span class="float-right badge badge-{{$notification.severity}} {{$notification.type}}-update"></span> + <div id="notifications-spinner-{{$notification.type}}" class="float-right spinner-wrapper"> + <div class="spinner s"></div> + </div> </a> - <div id="nav-{{$notification.type}}-sub" class="collapse notification-content" data-parent="#notifications" data-type="{{$notification.type}}"> + <div id="nav-{{$notification.type}}-sub" class="collapse notification-content" data-parent="#notifications" data-sse_type="{{$notification.type}}"> {{if $notification.viewall}} <a class="list-group-item text-dark" id="nav-{{$notification.type}}-see-all" href="{{$notification.viewall.url}}"> <i class="fa fa-fw fa-external-link"></i> {{$notification.viewall.label}} @@ -176,14 +180,16 @@ {{if $notification.filter.name_label}} <div class="list-group-item clearfix notifications-textinput" id="cn-{{$notification.type}}-only"> <div class="text-muted notifications-textinput-filter"><i class="fa fa-fw fa-filter"></i></div> - <input id="cn-{{$notification.type}}-input" type="text" class="form-control form-control-sm" placeholder="{{$notification.filter.name_label}}"> + <input id="cn-{{$notification.type}}-input" type="text" class="notification-filter form-control form-control-sm" placeholder="{{$notification.filter.name_label}}"> <div id="cn-{{$notification.type}}-input-clear" class="text-muted notifications-textinput-clear d-none"><i class="fa fa-times"></i></div> </div> {{/if}} {{/if}} - <div id="nav-{{$notification.type}}-menu" class=""> + <div id="nav-{{$notification.type}}-menu"></div> + <div id="nav-{{$notification.type}}-loading" style="display: none;"> {{$loading}}<span class="jumping-dots"><span class="dot-1">.</span><span class="dot-2">.</span><span class="dot-3">.</span></span> </div> + </div> </div> {{/foreach}} |