more stuff

This commit is contained in:
Pachira 2016-03-26 17:36:58 +01:00
parent 95f431a7d4
commit a0805cc29b
16 changed files with 280 additions and 205 deletions

View file

@ -75,11 +75,11 @@ class BBcode
['s', '<del>{param}</del>'],
['spoiler', '<span class="spoiler">{param}</span>'],
['box', '<div class="spoiler-box-container">
<div class="spoiler-box-title" onclick="Sakura.toggleClass(this.parentNode.children[1], \'hidden\');">
Click to open</div><div class="spoiler-box-content hidden">{param}</div></div>', ],
['box', '<div class="spoiler-box-container"><div class="spoiler-box-title"
onclick="Sakura.toggleClass(this.parentNode.children[1], \'hidden\');">{option}</div>
<div class="spoiler-box-content hidden">{param}</div></div>', ],
<div class="spoiler-box-title" onclick="Sakura.toggleClass(this.parentNode.children[1], \'hidden\');">'
. 'Click to open</div><div class="spoiler-box-content hidden">{param}</div></div>'],
['box', '<div class="spoiler-box-container"><div class="spoiler-box-title"'
. ' onclick="Sakura.toggleClass(this.parentNode.children[1], \'hidden\');">{option}</div>'
. '<div class="spoiler-box-content hidden">{param}</div></div>'],
['quote', '<blockquote><div class="quotee">Quote</div><div class="quote">{param}</div></blockquote>'],
];

View file

@ -36,10 +36,12 @@ class Code extends CodeDefinition
*/
public function asHtml(ElementNode $el)
{
return preg_replace(
"#\n*\[code\]\n*(.*?)\n*\[/code\]\n*#s",
'<pre class="code"><code>\\1</code></pre>',
$el->getAsBBCode()
);
$content = "";
foreach ($el->getChildren() as $child) {
$content .= $child->getAsBBCode();
}
return "<pre class='code'><code>{$content}</code></pre>";
}
}

View file

@ -9,6 +9,8 @@ namespace Sakura\Controllers;
use Sakura\Config;
use Sakura\DB;
use Sakura\Notification;
use Sakura\Perms\Site;
use Sakura\Rank;
use Sakura\Router;
use Sakura\Template;
@ -104,7 +106,7 @@ class UserController extends Controller
global $currentUser;
// Check permission
if (!$currentUser->permission(\Sakura\Perms\Site::VIEW_MEMBERLIST)) {
if (!$currentUser->permission(Site::VIEW_MEMBERLIST)) {
return Template::render('global/restricted');
}
@ -136,4 +138,42 @@ class UserController extends Controller
// Render the template
return Template::render('main/memberlist');
}
public function notifications()
{
// TODO: add friend on/offline messages
global $currentUser;
// Set json content type
header('Content-Type: application/json; charset=utf-8');
return json_encode(
$currentUser->notifications(),
JSON_FORCE_OBJECT | JSON_NUMERIC_CHECK | JSON_BIGINT_AS_STRING
);
}
public function markNotification($id = 0)
{
global $currentUser;
// Check permission
if ($currentUser->permission(Site::DEACTIVATED)) {
return '0';
}
// Create the notification object
$alert = new Notification($id);
// Verify that the currently authed user is the one this alert is for
if ($alert->user !== $currentUser->id) {
return '0';
}
// Toggle the read status and save
$alert->toggleRead();
$alert->save();
return '1';
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* Notification object.
*
* @package Sakura
*/
namespace Sakura;
/**
* Notification!
*
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class Notification
{
public $id = 0;
public $user = 0;
public $time = 0;
public $read = false;
public $title = "Notification";
public $text = "";
public $link = "";
public $image = "";
public $timeout = 0;
public function __construct($id = 0)
{
// Get notification data from the database
$data = DB::table('notifications')
->where('alert_id', $id)
->get();
// Check if anything was returned and assign data
if ($data) {
$data = $data[0];
$this->id = $data->alert_id;
$this->user = $data->user_id;
$this->time = $data->alert_timestamp;
$this->read = intval($data->alert_read) !== 0;
$this->title = $data->alert_title;
$this->text = $data->alert_text;
$this->link = $data->alert_link;
$this->image = $data->alert_img;
$this->timeout = $data->alert_timeout;
}
}
public function save()
{
// Create submission data, insert and update take the same format
$data = [
'user_id' => $this->user,
'alert_timestamp' => $this->time,
'alert_read' => $this->read ? 1 : 0,
'alert_title' => $this->title,
'alert_text' => $this->text,
'alert_link' => $this->link,
'alert_img' => $this->image,
'alert_timeout' => $this->timeout,
];
// Update if id isn't 0
if ($this->id) {
DB::table('notifications')
->where('alert_id', $this->id)
->update($data);
} else {
$this->id = DB::table('notifications')
->insertGetId($data);
}
}
public function toggleRead()
{
// Set read to the negative value of itself
$this->read = !$this->read;
}
}

View file

@ -136,8 +136,6 @@ class Router
*
* @param array $filters The filters for this group.
* @param \Closure $callback The containers
*
* @return string The generated URI.
*/
public static function group($filters, $callback)
{
@ -145,6 +143,17 @@ class Router
self::$router->group($filters, $callback);
}
/**
* Create filter.
*
* string $name Identifier of the filter
* \Closure $method
*/
public static function filter($name, $method)
{
self::$router->filter($name, $method);
}
/**
* Handle requests.
*

View file

@ -1194,4 +1194,35 @@ class User
// Return success
return [1, 'SUCCESS'];
}
/**
* Get all the notifications for this user.
*
* @param int $timeDifference The timeframe of alerts that should be fetched.
* @param bool $excludeRead Whether alerts that are marked as read should be included.
*
* @return array An array with Notification objects.
*/
public function notifications($timeDifference = 0, $excludeRead = true)
{
$alertIds = DB::table('notifications')
->where('user_id', $this->id);
if ($timeDifference) {
$alertIds->where('alert_timestamp', '>', time() - $timeDifference);
}
if ($excludeRead) {
$alertIds->where('alert_read', 0);
}
$alertIds = array_column($alertIds->get(['alert_id']), 'alert_id');
$alerts = [];
foreach ($alertIds as $alertId) {
$alerts[$alertId] = new Notification($alertId);
}
return $alerts;
}
}

View file

@ -350,92 +350,6 @@ class Users
}
}
/**
* Get a user's notifications.
*
* @param int $uid The user id.
* @param int $timediff The maximum difference in time.
* @param bool $excludeRead Exclude notifications that were already read.
* @param bool $markRead Automatically mark as read.
*
* @return array The notifications.
*/
public static function getNotifications($uid = null, $timediff = 0, $excludeRead = true, $markRead = false)
{
// Prepare conditions
$uid = $uid ? $uid : self::checkLogin()[0];
$time = $timediff ? time() - $timediff : '%';
$read = $excludeRead ? '0' : '%';
// Get notifications for the database
$alerts = DB::table('notifications')
->where('user_id', $uid)
->where('alert_timestamp', '>', $time)
->where('alert_read', $read)
->get();
// Mark the notifications as read
if ($markRead) {
// Iterate over all entries
foreach ($alerts as $alert) {
// If the notifcation is already read skip
if ($alert->alert_read) {
continue;
}
// Mark them as read
self::markNotificationRead($notification->alert_id);
}
}
// Return the notifications
return $notifications;
}
/**
* Mark a notification as read
*
* @param mixed $id The notification's ID.
* @param mixed $mode Read or unread.
*/
public static function markNotificationRead($id, $mode = true)
{
// Execute an update statement
DB::table('notifications')
->where('alert_id', $id)
->update([
'alert_read' => ($mode ? 1 : 0),
]);
}
/**
* Create a new notification.
*
* @param int $user The user id.
* @param string $title The notification title.
* @param string $text The rest of the text.
* @param int $timeout After how many seconds the notification should disappear.
* @param string $img The image.
* @param string $link The link.
* @param int $sound Whether it should play a noise.
*/
public static function createNotification($user, $title, $text, $timeout = 60000, $img = 'FONT:fa-info-circle', $link = '', $sound = 0)
{
// Insert it into the database
DB::table('notifications')
->insert([
'user_id' => $user,
'alert_timestamp' => time(),
'alert_read' => 0,
'alert_sound' => ($sound ? 1 : 0),
'alert_title' => $title,
'alert_text' => $text,
'alert_link' => $link,
'alert_img' => $img,
'alert_timeout' => $timeout,
]);
}
/**
* Get the newest member's ID.
*

View file

@ -20,18 +20,18 @@ function notifyUI(content) {
alert.className = 'notification-enter';
alert.id = id;
// Add the icon
if ((typeof content.img).toLowerCase() === 'undefined' || content.img == null || content.img.length < 2) {
if ((typeof content.image).toLowerCase() === 'undefined' || content.image == null || content.image.length < 2) {
aIconCont = document.createElement('div');
aIconCont.className = 'font-icon fa fa-info fa-4x';
}
else if (content.img.substr(0, 5) == 'FONT:') {
else if (content.image.substr(0, 5) == 'FONT:') {
aIconCont = document.createElement('div');
aIconCont.className = 'font-icon fa ' + content.img.replace('FONT:', '') + ' fa-4x';
aIconCont.className = 'font-icon fa ' + content.image.replace('FONT:', '') + ' fa-4x';
}
else {
aIconCont = document.createElement('img');
aIconCont.alt = id;
aIconCont.src = content.img;
aIconCont.src = content.image;
}
aIcon.appendChild(aIconCont);
aIcon.className = 'notification-icon';
@ -58,25 +58,6 @@ function notifyUI(content) {
alert.appendChild(aClose);
// Append the notification to the document
cont.appendChild(alert);
// Play sound if request
if (content.sound) {
// Create the elements
var sound = document.createElement('audio');
var mp3 = document.createElement('source');
var ogg = document.createElement('source');
// Assign attribs
mp3.type = 'audio/mp3';
ogg.type = 'audio/ogg';
mp3.src = sakuraVars.content + '/sounds/notify.mp3';
ogg.src = sakuraVars.content + '/sounds/notify.ogg';
// Append
sound.appendChild(mp3);
sound.appendChild(ogg);
// Less loud
sound.volume = 0.5;
// And play
sound.play();
}
// If keepalive is 0 keep the notification open forever
if (content.timeout > 0) {
// Set a timeout and close after an amount
@ -111,7 +92,7 @@ function notifyRequest(session) {
}
// Create AJAX object
var get = new AJAX();
get.setUrl('/settings.php?request-notifications=true&time=' + Sakura.epoch() + '&session=' + session);
get.setUrl('/notifications');
// Add callbacks
get.addCallback(200, function () {
// Assign the parsed JSON

View file

@ -8,9 +8,8 @@ interface Notification {
title: string;
text: string;
link: string;
img: string;
image: string;
timeout: number;
sound: boolean;
}
// Spawns a notification
@ -29,22 +28,22 @@ function notifyUI(content: Notification): void {
var aCIcon: HTMLDivElement = document.createElement('div');
var aClear: HTMLDivElement = document.createElement('div');
var aIconCont: any;
// Add attributes to the main element
alert.className = 'notification-enter';
alert.id = id;
// Add the icon
if ((typeof content.img).toLowerCase() === 'undefined' || content.img == null || content.img.length < 2) {
if ((typeof content.image).toLowerCase() === 'undefined' || content.image == null || content.image.length < 2) {
aIconCont = document.createElement('div');
aIconCont.className = 'font-icon fa fa-info fa-4x';
} else if (content.img.substr(0, 5) == 'FONT:') {
} else if (content.image.substr(0, 5) == 'FONT:') {
aIconCont = document.createElement('div');
aIconCont.className = 'font-icon fa ' + content.img.replace('FONT:', '') + ' fa-4x';
aIconCont.className = 'font-icon fa ' + content.image.replace('FONT:', '') + ' fa-4x';
} else {
aIconCont = document.createElement('img');
aIconCont.alt = id;
aIconCont.src = content.img;
aIconCont.src = content.image;
}
aIcon.appendChild(aIconCont);
@ -78,30 +77,6 @@ function notifyUI(content: Notification): void {
// Append the notification to the document
cont.appendChild(alert);
// Play sound if request
if (content.sound) {
// Create the elements
var sound: HTMLAudioElement = document.createElement('audio');
var mp3: HTMLSourceElement = document.createElement('source');
var ogg: HTMLSourceElement = document.createElement('source');
// Assign attribs
mp3.type = 'audio/mp3';
ogg.type = 'audio/ogg';
mp3.src = sakuraVars.content + '/sounds/notify.mp3';
ogg.src = sakuraVars.content + '/sounds/notify.ogg';
// Append
sound.appendChild(mp3);
sound.appendChild(ogg);
// Less loud
sound.volume = 0.5;
// And play
sound.play();
}
// If keepalive is 0 keep the notification open forever
if (content.timeout > 0) {
// Set a timeout and close after an amount
@ -128,7 +103,7 @@ function notifyClose(id: string): void {
// Opening an alerted link
function notifyOpen(id: string): void {
var sakuraHref: string = document.getElementById(id).getAttribute('sakurahref');
if ((typeof sakuraHref).toLowerCase() !== 'undefined') {
window.location.assign(sakuraHref);
}
@ -143,7 +118,7 @@ function notifyRequest(session: string): void {
// Create AJAX object
var get: AJAX = new AJAX();
get.setUrl('/settings.php?request-notifications=true&time=' + Sakura.epoch() + '&session=' + session);
get.setUrl('/notifications');
// Add callbacks
get.addCallback(200, () => {

Binary file not shown.

Binary file not shown.

View file

@ -17,7 +17,7 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification
// Include components
require_once str_replace(basename(__DIR__), '', dirname(__FILE__)) . 'sakura.php';
// Notifications
// Notifications (decommissioned)
if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notifications']) {
// Create the notification container array
$notifications = [];
@ -28,20 +28,23 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification
&& $_REQUEST['time'] > (time() - 1000)
&& isset($_REQUEST['session']) && $_REQUEST['session'] == session_id()) {
// Get the user's notifications from the past forever but exclude read notifications
$userNotifs = Users::getNotifications(null, 0, true, true);
$alerts = $currentUser->notifications();
// Add the proper values to the array
foreach ($userNotifs as $notif) {
foreach ($alerts as $alert) {
// Add the notification to the display array
$notifications[$notif['alert_timestamp']] = [
'read' => $notif['alert_read'],
'title' => $notif['alert_title'],
'text' => $notif['alert_text'],
'link' => $notif['alert_link'],
'img' => $notif['alert_img'],
'timeout' => $notif['alert_timeout'],
'sound' => $notif['alert_sound'],
$notifications[$alert->id] = [
'read' => $alert->read,
'title' => $alert->title,
'text' => $alert->text,
'link' => $alert->link,
'img' => $alert->image,
'timeout' => $alert->timeout,
'sound' => $alert->sound,
];
$alert->toggleRead();
$alert->save();
}
}
@ -71,7 +74,7 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification
'title' => $friend->username . ' is online.',
'text' => '',
'link' => '',
'img' => '/a/' . $friend->id,
'img' => Router::route('file.avatar', $friend->id),
'timeout' => 2000,
'sound' => false,
];
@ -87,7 +90,7 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification
'title' => $friend->username . ' is offline.',
'text' => '',
'link' => '',
'img' => '/a/' . $friend->id,
'img' => Router::route('file.avatar', $friend->id),
'timeout' => 2000,
'sound' => false,
];
@ -377,16 +380,20 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification
if (array_key_exists($action[1], $notifStrings)) {
// Get the current user's profile data
$user = User::construct($currentUser->id);
$friend = User::construct($_REQUEST[(isset($_REQUEST['add']) ? 'add' : 'remove')]);
Users::createNotification(
$_REQUEST[(isset($_REQUEST['add']) ? 'add' : 'remove')],
sprintf($notifStrings[$action[1]][0], $user->username),
$notifStrings[$action[1]][1],
60000,
Router::route('file.avatar', $user->id),
Router::route('user.profile', $user->id),
'1'
);
$alert = new Notification;
$alert->user = $friend->id;
$alert->time = time();
$alert->sound = true;
$alert->title = sprintf($notifStrings[$action[1]][0], $user->username);
$alert->text = $notifStrings[$action[1]][1];
$alert->image = Router::route('file.avatar', $user->id);
$alert->timeout = 60000;
$alert->link = Router::route('user.profile', $user->id);
$alert->save();
}
}
@ -1490,11 +1497,6 @@ if (Users::checkLogin()) {
$renderData['messages'] = [];
break;
// Notification history
case 'notifications.history':
$renderData['alerts'] = array_reverse(Users::getNotifications(null, 0, false, true));
break;
// Avatar and background sizes
case 'appearance.avatar':
case 'appearance.background':

View file

@ -6,6 +6,32 @@
// Define namespace
namespace Sakura;
// Check if logged out
Router::filter('logoutCheck', function () {
global $currentUser;
if ($currentUser->id !== 0) {
$message = "You must be logged out to do that!";
Template::vars(['page' => compact('message')]);
return Template::render('global/information');
}
});
// Check if logged in
Router::filter('loginCheck', function () {
global $currentUser;
if ($currentUser->id === 0) {
$message = "You must be logged in to do that!";
Template::vars(['page' => compact('message')]);
return Template::render('global/information');
}
});
// Meta pages
Router::get('/', 'MetaController@index', 'main.index');
Router::get('/faq', 'MetaController@faq', 'main.faq');
@ -13,16 +39,20 @@ Router::get('/search', 'MetaController@search', 'main.search');
Router::get('/p/{id}', 'MetaController@infoPage', 'main.infopage');
// Auth
Router::get('/login', 'AuthController@loginGet', 'auth.login');
Router::post('/login', 'AuthController@loginPost', 'auth.login');
Router::get('/logout', 'AuthController@logout', 'auth.logout');
Router::get('/register', 'AuthController@registerGet', 'auth.register');
Router::post('/register', 'AuthController@registerPost', 'auth.register');
Router::get('/resetpassword', 'AuthController@resetPasswordGet', 'auth.resetpassword');
Router::post('/resetpassword', 'AuthController@resetPasswordPost', 'auth.resetpassword');
Router::get('/reactivate', 'AuthController@reactivateGet', 'auth.reactivate');
Router::post('/reactivate', 'AuthController@reactivatePost', 'auth.reactivate');
Router::get('/activate', 'AuthController@activate', 'auth.activate');
Router::group(['before' => 'logoutCheck'], function () {
Router::get('/login', 'AuthController@loginGet', 'auth.login');
Router::post('/login', 'AuthController@loginPost', 'auth.login');
Router::get('/register', 'AuthController@registerGet', 'auth.register');
Router::post('/register', 'AuthController@registerPost', 'auth.register');
Router::get('/resetpassword', 'AuthController@resetPasswordGet', 'auth.resetpassword');
Router::post('/resetpassword', 'AuthController@resetPasswordPost', 'auth.resetpassword');
Router::get('/reactivate', 'AuthController@reactivateGet', 'auth.reactivate');
Router::post('/reactivate', 'AuthController@reactivatePost', 'auth.reactivate');
Router::get('/activate', 'AuthController@activate', 'auth.activate');
});
Router::group(['before' => 'loginCheck'], function () {
Router::get('/logout', 'AuthController@logout', 'auth.logout');
});
// News
Router::group(['prefix' => 'news'], function () {
@ -37,9 +67,11 @@ Router::group(['prefix' => 'forum'], function () {
Router::group(['prefix' => 'post'], function () {
Router::get('/{id:i}', 'ForumController@post', 'forums.post');
Router::get('/{id:i}/raw', 'ForumController@postRaw', 'forums.post.raw');
Router::get('/{id:i}/delete', 'ForumController@deletePost', 'forums.post.delete');
Router::post('/{id:i}/delete', 'ForumController@deletePost', 'forums.post.delete');
Router::post('/{id:i}/edit', 'ForumController@editPost', 'forums.post.edit');
Router::group(['before' => 'loginCheck'], function () {
Router::get('/{id:i}/delete', 'ForumController@deletePost', 'forums.post.delete');
Router::post('/{id:i}/delete', 'ForumController@deletePost', 'forums.post.delete');
Router::post('/{id:i}/edit', 'ForumController@editPost', 'forums.post.edit');
});
});
// Thread
@ -66,6 +98,8 @@ Router::group(['prefix' => 'members'], function () {
// User
Router::get('/u/{id}', 'UserController@profile', 'user.profile');
Router::get('/u/{id}/header', 'FileController@header', 'user.header');
Router::get('/notifications', 'UserController@notifications', 'user.notifications');
Router::get('/notifications/{id}/mark', 'UserController@markNotification', 'user.notifications.mark');
// Files
Router::get('/a/{id}', 'FileController@avatar', 'file.avatar');

View file

@ -8,7 +8,7 @@
namespace Sakura;
// Define Sakura version
define('SAKURA_VERSION', 20160325);
define('SAKURA_VERSION', 20160326);
// Define Sakura Path
define('ROOT', __DIR__ . '/');

View file

@ -212,6 +212,12 @@
timeElems[timeElem].innerText = Sakura.timeElapsed(Math.floor(parsed / 1000));
}
}
notifyRequest('{{ php.sessionid }}');
setInterval(function() {
notifyRequest('{{ php.sessionid }}');
}, 60000);
</script>
{% if sakura.dev.showChangelog and stats %}
<script type="text/javascript" src="https://sakura.flash.moe/?get=all&amp;limit=5&amp;variable=true"></script>

View file

@ -1,4 +1,4 @@
{% set alerts = alerts|batch(10) %}
{% set alerts = user.notifications(0, false)|batch(10) %}
{% set paginationPages = alerts %}
{% set paginationUrl %}{{ urls.format('SETTING_MODE', ['notifications', 'history']) }}{% endset %}
@ -14,25 +14,25 @@
{% if alerts %}
<div class="notification-history">
{% for alert in alerts[get.page|default(1) - 1] %}
<a id="notif-hist-{{ alert.alert_id }}" class="clean {% if alert.alert_read %}read{% endif %}"{% if alert.alert_link %} href="{{ alert.alert_link }}"{% endif %}>
<a id="notif-hist-{{ alert.id }}" class="clean {% if alert.read %}read{% endif %}"{% if alert.link %} href="{{ alert.link }}"{% endif %}>
<div class="notif-hist-icon">
{% if 'FONT:' in alert.alert_img %}
<div class="font-icon fa {{ alert.alert_img|replace({'FONT:': ''}) }} fa-4x"></div>
{% if 'FONT:' in alert.image %}
<div class="font-icon fa {{ alert.image|replace({'FONT:': ''}) }} fa-4x"></div>
{% else %}
<img src="{{ alert.alert_img }}" alt="Notification" />
<img src="{{ alert.image }}" alt="Notification" />
{% endif %}
</div>
<div class="notif-hist-content">
<div class="notif-hist-inside">
<div class="notif-hist-title">
{{ alert.alert_title }}
{{ alert.title }}
</div>
<div class="notif-hist-text">
{{ alert.alert_text }}
{{ alert.title }}
</div>
</div>
<div class="notif-hist-time">
<time>{{ alert.alert_timestamp|date(sakura.dateFormat) }}</time>
<time>{{ alert.time|date(sakura.dateFormat) }}</time>
</div>
</div>
<div class="clear"></div>